Compare commits
416 Commits
open-relea
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48122ff99d | ||
|
|
3153cff4ff | ||
|
|
919d98df79 | ||
|
|
c7adcecb8a | ||
|
|
db4eeac266 | ||
|
|
60e2119116 | ||
|
|
7b4714a22a | ||
|
|
332d6abee7 | ||
|
|
4df13cf0b7 | ||
|
|
512deae883 | ||
|
|
2d11477037 | ||
|
|
5e15969f4a | ||
|
|
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 | ||
|
|
8549bf2fcf | ||
|
|
762da62a29 | ||
|
|
7d536e6cdb | ||
|
|
166d7953c9 | ||
|
|
8ea457f949 | ||
|
|
35dfc720c8 | ||
|
|
3ca2739fce | ||
|
|
5df3d8f6e2 | ||
|
|
980a4c4003 | ||
|
|
c3f5374c6e | ||
|
|
fcccc19fc5 | ||
|
|
c2609a3316 | ||
|
|
34f3240c03 | ||
|
|
dba4792a7d | ||
|
|
ad13f7e5c6 | ||
|
|
20476b9445 | ||
|
|
21ad3d78a4 | ||
|
|
8b8e41fa1b | ||
|
|
f997cd0b47 | ||
|
|
9241be296f | ||
|
|
47c2d5c15e | ||
|
|
ed89d8546c | ||
|
|
19de32cb5d | ||
|
|
08a859c002 | ||
|
|
ff1db82c56 | ||
|
|
c122afefb9 | ||
|
|
689542947e | ||
|
|
f64aeff6b5 | ||
|
|
4dd12f6230 | ||
|
|
4571d35102 | ||
|
|
ed7aca7bc5 | ||
|
|
f96dc974f3 | ||
|
|
81f3dbf584 | ||
|
|
4f91b30ee1 | ||
|
|
51272c0fc4 | ||
|
|
27d4a26ae8 | ||
|
|
2fdf630ed3 | ||
|
|
dc056a6cc0 | ||
|
|
54015223a2 | ||
|
|
345777cb72 | ||
|
|
347493fe6b | ||
|
|
766438dcf3 | ||
|
|
f2989e836a | ||
|
|
34118bfc90 | ||
|
|
91a74e309a | ||
|
|
0c32b98e85 | ||
|
|
e059fab172 | ||
|
|
08a8bc0e4f | ||
|
|
3dc0cd97ca | ||
|
|
697adecc1d | ||
|
|
1063945825 | ||
|
|
8aaf4c676f | ||
|
|
10e6abad18 | ||
|
|
a69aa06123 | ||
|
|
81a5857d07 | ||
|
|
cc5cc30279 | ||
|
|
0d492cc9d9 | ||
|
|
5440de3684 | ||
|
|
42768f1f09 | ||
|
|
0d168d79b2 | ||
|
|
c90b8b702b | ||
|
|
db64ff2098 | ||
|
|
1d96fa6146 | ||
|
|
ce72bfdb68 | ||
|
|
7ac8f6a40a | ||
|
|
1cb0cfa113 | ||
|
|
e2899e66d1 | ||
|
|
7229d87fd1 | ||
|
|
fcbabe95ea | ||
|
|
4a0b23b7a1 | ||
|
|
f9190549ff | ||
|
|
79774b1d47 | ||
|
|
fce5b23763 | ||
|
|
0af61b1387 | ||
|
|
e48ab11d5b | ||
|
|
9f92c412b4 | ||
|
|
2b30e0998c | ||
|
|
b8443d517e | ||
|
|
e7dd047eb5 | ||
|
|
3541c66637 | ||
|
|
028e0b0dfc | ||
|
|
1ce7962fa5 | ||
|
|
70a20726a9 | ||
|
|
f83c24c020 | ||
|
|
d4bfdf699b | ||
|
|
1ee501f8b9 | ||
|
|
13d67eb2a3 | ||
|
|
a9558cb296 | ||
|
|
7706d44667 | ||
|
|
3486aead7f | ||
|
|
2947784f00 | ||
|
|
7acb102b43 | ||
|
|
4bd1f3de7d | ||
|
|
5f2570c440 | ||
|
|
f066880c7c | ||
|
|
1eded91f24 | ||
|
|
029f201a46 | ||
|
|
6f5a6f6500 | ||
|
|
9c555c06be | ||
|
|
9198b122ec | ||
|
|
5a3ee877d6 | ||
|
|
3c049ab65a |
31
.env
31
.env
@@ -13,18 +13,29 @@ ORDER_HISTORY_URL=null
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT=null
|
||||
SEGMENT_KEY=''
|
||||
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=''
|
||||
# ***** Cookies *****
|
||||
USER_RETENTION_COOKIE_NAME=null
|
||||
# ***** Links *****
|
||||
LOGIN_ISSUE_SUPPORT_LINK=''
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null
|
||||
POST_REGISTRATION_REDIRECT_URL=''
|
||||
SEARCH_CATALOG_URL=''
|
||||
# ***** Features flags *****
|
||||
DISABLE_ENTERPRISE_LOGIN=''
|
||||
REGISTER_CONVERSION_COOKIE_NAME=null
|
||||
ENABLE_PROGRESSIVE_PROFILING=''
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS=''
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN=''
|
||||
ENABLE_POST_REGISTRATION_RECOMMENDATIONS=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
SHOW_DYNAMIC_PROFILING_PAGE=''
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS=''
|
||||
# ***** Zendesk related keys *****
|
||||
ZENDESK_KEY=''
|
||||
ZENDESK_LOGO_URL=''
|
||||
# ***** Base Container Images *****
|
||||
BANNER_IMAGE_LARGE=''
|
||||
BANNER_IMAGE_MEDIUM=''
|
||||
BANNER_IMAGE_SMALL=''
|
||||
BANNER_IMAGE_EXTRA_SMALL=''
|
||||
# ***** Miscellaneous *****
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
@@ -18,18 +18,23 @@ ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME='Your Platform Name Here'
|
||||
INFO_EMAIL='info@example.com'
|
||||
# ***** Cookies *****
|
||||
SESSION_COOKIE_DOMAIN='localhost'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
AUTHN_MINIMAL_HEADER=true
|
||||
LOGIN_ISSUE_SUPPORT_LINK='/login-issue-support-url'
|
||||
# ***** Links *****
|
||||
LOGIN_ISSUE_SUPPORT_LINK='http://localhost:18000/login-issue-support-url'
|
||||
TOS_AND_HONOR_CODE='http://localhost:18000/honor'
|
||||
TOS_LINK='http://localhost:18000/tos'
|
||||
PRIVACY_POLICY='http://localhost:18000/privacy'
|
||||
USER_SURVEY_COOKIE_NAME='openedx-user-survey-type'
|
||||
COOKIE_DOMAIN='localhost'
|
||||
WELCOME_PAGE_SUPPORT_LINK='http://localhost:1999/welcome'
|
||||
INFO_EMAIL='info@edx.org'
|
||||
DISABLE_ENTERPRISE_LOGIN=''
|
||||
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK='http://localhost:1999/welcome'
|
||||
# ***** Base Container Images *****
|
||||
BANNER_IMAGE_LARGE=''
|
||||
BANNER_IMAGE_MEDIUM=''
|
||||
BANNER_IMAGE_SMALL=''
|
||||
BANNER_IMAGE_EXTRA_SMALL=''
|
||||
# ***** Miscellaneous *****
|
||||
APP_ID=''
|
||||
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'
|
||||
12
.env.test
12
.env.test
@@ -16,13 +16,5 @@ ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=''
|
||||
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'
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
ZENDESK_KEY=''
|
||||
ZENDESK_LOGO_URL=''
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
coverage/*
|
||||
dist/
|
||||
docs
|
||||
node_modules/
|
||||
__mocks__/
|
||||
__snapshots__/
|
||||
|
||||
36
.eslintrc.js
36
.eslintrc.js
@@ -1,16 +1,17 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('eslint', {
|
||||
rules: {
|
||||
// Temporarily update the 'indent', 'template-curly-spacing' and
|
||||
// 'no-multiple-empty-lines' rules since they are causing eslint
|
||||
// to fail for no apparent reason since upgrading
|
||||
// to fail for no apparent reason since upgrading
|
||||
// @edx/frontend-build from v3 to v5:
|
||||
// - TypeError: Cannot read property 'range' of null
|
||||
'indent': [
|
||||
indent: [
|
||||
'error',
|
||||
2,
|
||||
{ 'ignoredNodes': ['TemplateLiteral', 'SwitchCase'] }
|
||||
{ ignoredNodes: ['TemplateLiteral', 'SwitchCase'] },
|
||||
],
|
||||
'template-curly-spacing': 'off',
|
||||
'jsx-a11y/label-has-associated-control': ['error', {
|
||||
@@ -18,7 +19,34 @@ module.exports = createConfig('eslint', {
|
||||
labelAttributes: [],
|
||||
controlComponents: [],
|
||||
assert: 'htmlFor',
|
||||
depth: 25
|
||||
depth: 25,
|
||||
}],
|
||||
'sort-imports': ['error', { ignoreCase: true, ignoreDeclarationSort: true }],
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
'builtin',
|
||||
'external',
|
||||
'internal',
|
||||
['sibling', 'parent'],
|
||||
'index',
|
||||
],
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: '@(react|react-dom|react-redux)',
|
||||
group: 'external',
|
||||
position: 'before',
|
||||
},
|
||||
],
|
||||
pathGroupsExcludedImportTypes: ['react'],
|
||||
'newlines-between': 'always',
|
||||
alphabetize: {
|
||||
order: 'asc',
|
||||
caseInsensitive: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
'function-paren-newline': 'off',
|
||||
},
|
||||
});
|
||||
|
||||
29
.github/pull_request_template.md
vendored
Normal file
29
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
### Description
|
||||
|
||||
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?
|
||||
|
||||
Please describe in detail how you tested your changes.
|
||||
|
||||
#### 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.**
|
||||
|
||||
|Before|After|
|
||||
|-------|-----|
|
||||
| | |
|
||||
|
||||
#### Merge Checklist
|
||||
|
||||
* [ ] If your update includes visual changes, have they been reviewed by a designer? Send them a link to the Sandbox, if applicable.
|
||||
* [ ] Is there adequate test coverage for your changes?
|
||||
|
||||
#### Post-merge Checklist
|
||||
|
||||
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/vanguards** to do it.
|
||||
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.
|
||||
@@ -16,4 +16,4 @@ jobs:
|
||||
secrets:
|
||||
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
|
||||
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"
|
||||
18
.github/workflows/ci.yml
vendored
18
.github/workflows/ci.yml
vendored
@@ -11,17 +11,16 @@ on:
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16]
|
||||
|
||||
steps:
|
||||
- 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
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
@@ -41,8 +40,5 @@ jobs:
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Verify Es5
|
||||
run: npm run is-es5
|
||||
|
||||
- name: Run Code Coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
2
.github/workflows/commitlint.yml
vendored
2
.github/workflows/commitlint.yml
vendored
@@ -7,4 +7,4 @@ on:
|
||||
|
||||
jobs:
|
||||
commitlint:
|
||||
uses: edx/.github/.github/workflows/commitlint.yml@master
|
||||
uses: openedx/.github/.github/workflows/commitlint.yml@master
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: edx/.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
|
||||
coverage
|
||||
module.config.js
|
||||
.env.private
|
||||
|
||||
dist/
|
||||
src/i18n/transifex_input.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# The following users are the owners of all frontend-app-authn files
|
||||
* @edx/vanguards
|
||||
* @openedx/vanguards
|
||||
|
||||
33
Makefile
Executable file → Normal file
33
Makefile
Executable file → Normal file
@@ -1,6 +1,7 @@
|
||||
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
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
@@ -42,11 +43,37 @@ push_translations:
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||
|
||||
ifeq ($(OPENEDX_ATLAS_PULL),)
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -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:
|
||||
# Checking for package-lock.json changes...
|
||||
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
|
||||
|
||||
188
README.rst
188
README.rst
@@ -1,48 +1,190 @@
|
||||
|Build Status| |Codecov| |license|
|
||||
|Build Status| |ci-badge| |Codecov| |semantic-release|
|
||||
|
||||
frontend-app-authn
|
||||
=================================
|
||||
====================
|
||||
|
||||
Please tag **@openedx/vanguards** on any PRs or issues. Thanks!
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
This is a micro-frontend application responsible for the login, registration and password reset functionality.
|
||||
|
||||
Development
|
||||
-----------
|
||||
**What is the domain of this MFE?**
|
||||
|
||||
Start Devstack
|
||||
^^^^^^^^^^^^^^
|
||||
- Register page
|
||||
|
||||
To use this application `devstack <https://github.com/edx/devstack>`__ must be running.
|
||||
- Login page
|
||||
|
||||
- Start devstack
|
||||
- Forgot password page
|
||||
|
||||
Start the development server
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Reset password page
|
||||
|
||||
In this project, install requirements and start the development server by running:
|
||||
- Progressive profiling page
|
||||
|
||||
.. code:: bash
|
||||
|
||||
npm install
|
||||
npm start # The server will run on port 1999
|
||||
Installation
|
||||
------------
|
||||
|
||||
Once the dev server is up visit http://localhost:1999/login.
|
||||
This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ section for setup instructions.
|
||||
|
||||
Configuration and Deployment
|
||||
----------------------------
|
||||
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
|
||||
|
||||
This MFE is configured via node environment variables supplied at build time. See the .env file for the list of required environment variables. Example build syntax with a single environment variable:
|
||||
2. Start up LMS, if it's not already started.
|
||||
|
||||
.. code:: bash
|
||||
4. Within this project (frontend-app-authn), install requirements and start the development server:
|
||||
|
||||
NODE_ENV=development ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' npm run build
|
||||
.. code-block::
|
||||
|
||||
npm install
|
||||
npm start # The server will run on port 1999
|
||||
|
||||
5. Once the dev server is up, visit http://localhost:1999 to access the MFE
|
||||
|
||||
.. image:: ./docs/images/frontend-app-authn-localhost-preview.png
|
||||
|
||||
**Note:** Follow `Enable social auth locally <docs/how_tos/enable_social_auth.rst>`_ for enabling Social Sign-on Buttons (SSO) locally
|
||||
|
||||
Environment Variables/Setup Notes
|
||||
---------------------------------
|
||||
|
||||
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
|
||||
|
||||
The authentication micro-frontend also requires the following additional variable:
|
||||
|
||||
.. list-table:: Environment Variables
|
||||
:widths: 30 50 20
|
||||
:header-rows: 1
|
||||
|
||||
* - Name
|
||||
- Description / Usage
|
||||
- Example
|
||||
|
||||
* - ``LOGIN_ISSUE_SUPPORT_LINK``
|
||||
- The fully-qualified URL to the login issue support page in the target environment.
|
||||
- ``https://support.example.com``
|
||||
|
||||
* - ``ACTIVATION_EMAIL_SUPPORT_LINK``
|
||||
- The fully-qualified URL to the activation email support page in the target environment.
|
||||
- ``https://support.example.com``
|
||||
|
||||
* - ``PASSWORD_RESET_SUPPORT_LINK``
|
||||
- The fully-qualified URL to the password reset support page in the target environment.
|
||||
- ``https://support.example.com``
|
||||
|
||||
* - ``AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK``
|
||||
- The fully-qualified URL to the progressive profiling support page in the target environment.
|
||||
- ``https://support.example.com``
|
||||
|
||||
* - ``TOS_AND_HONOR_CODE``
|
||||
- The fully-qualified URL to the Honor code page in the target environment.
|
||||
- ``https://example.com/honor``
|
||||
|
||||
* - ``TOS_LINK``
|
||||
- The fully-qualified URL to the Terms of service page in the target environment.
|
||||
- ``https://example.com/tos``
|
||||
|
||||
* - ``PRIVACY_POLICY``
|
||||
- The fully-qualified URL to the Privacy policy page in the target environment.
|
||||
- ``https://example.com/privacy``
|
||||
|
||||
* - ``INFO_EMAIL``
|
||||
- The valid email address for information query regarding the target environment.
|
||||
- ``info@example.com``
|
||||
|
||||
* - ``ENABLE_DYNAMIC_REGISTRATION_FIELDS``
|
||||
- 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)
|
||||
|
||||
* - ``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.
|
||||
- ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
* - ``DISABLE_ENTERPRISE_LOGIN``
|
||||
- Disables the enterprise login from Authn MFE.
|
||||
- ``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
|
||||
**********************************
|
||||
|
||||
Furthermore, there are several edX-specific environment variables that enable integrations with closed-source services private to the edX organization, and might be unsupported in Open edX.
|
||||
|
||||
.. list-table:: edX-specific Environment Variables
|
||||
:widths: 30 50 20
|
||||
:header-rows: 1
|
||||
|
||||
* - Name
|
||||
- Description / Usage
|
||||
- Example
|
||||
|
||||
* - ``MARKETING_EMAILS_OPT_IN``
|
||||
- Enables support for opting in marketing emails that helps us getting user consent for sending marketing emails.
|
||||
- ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
* - ``SHOW_CONFIGURABLE_EDX_FIELDS``
|
||||
- For edX, country and honor code fields are required by default. This flag enables edX specific required fields.
|
||||
- ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
For more information see the document: `Micro-frontend applications in Open
|
||||
edX <https://github.com/edx/edx-developer-docs/blob/5191e800bf16cf42f25c58c58f983bdaf7f9305d/docs/micro-frontends-in-open-edx.rst>`__.
|
||||
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
|
||||
------------
|
||||
|
||||
None
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
The code in this repository is licensed under the GNU Affero General Public License v3.0, unless
|
||||
otherwise noted.
|
||||
|
||||
Please see `LICENSE <https://github.com/openedx/frontend-app-authn/blob/master/LICENSE>`_ for details.
|
||||
|
||||
==============================
|
||||
|
||||
.. |Build Status| image:: https://api.travis-ci.com/edx/frontend-app-authn.svg?branch=master
|
||||
:target: https://travis-ci.com/edx/frontend-app-authn
|
||||
.. |Codecov| image:: https://img.shields.io/codecov/c/github/edx/frontend-app-authn
|
||||
:target: https://codecov.io/gh/edx/frontend-app-authn
|
||||
.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-app-authn.svg
|
||||
:target: @edx/frontend-app-authn
|
||||
.. |ci-badge| image:: https://github.com/openedx/edx-developer-docs/actions/workflows/ci.yml/badge.svg
|
||||
:target: https://github.com/openedx/edx-developer-docs/actions/workflows/ci.yml
|
||||
:alt: Continuous Integration
|
||||
.. |semantic-release| image:: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
|
||||
:target: https://github.com/semantic-release/semantic-release
|
||||
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
|
||||
/data // Note: most files here are named with a plural, as they contain many of the things in question.
|
||||
/actions.js
|
||||
/constants.js
|
||||
/mockedData.js
|
||||
/reducers.js
|
||||
/sagas.js
|
||||
/selectors.js
|
||||
|
||||
14
docs/how_tos/enable_social_auth.rst
Normal file
14
docs/how_tos/enable_social_auth.rst
Normal file
@@ -0,0 +1,14 @@
|
||||
Enable Social Auth Locally
|
||||
--------------------------
|
||||
|
||||
Please follow the steps below to enable social auth (SSO) locally.
|
||||
|
||||
1. Follow `Enabling Third Party Authentication <https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/tpa/index.html>`_ for backend configuration.
|
||||
|
||||
2. Authn has a component for rendering Social Auth providers at frontend which goes through each provider.
|
||||
|
||||
* If the provider has an ``iconImage``, then it will be rendered as image in SSO button.
|
||||
|
||||
* If ``iconImage`` is not available in provider, but the provider's ``iconClass`` is from the supported icon classes ``['apple', 'facebook', 'google', 'microsoft']`` then it is used as icon image.
|
||||
|
||||
* If ``iconClass`` doesn't match the supported icon classes then the ``faSignInAlt`` from font awesome icons is used as icon image for SSO button.
|
||||
@@ -2,4 +2,4 @@
|
||||
React App i18n HOWTO
|
||||
####################
|
||||
|
||||
This document has moved to the frontend-platform repo: https://github.com/edx/frontend-platform/blob/master/docs/how_tos/i18n.rst
|
||||
This document has moved to the frontend-platform repo: https://github.com/openedx/frontend-platform/blob/master/docs/how_tos/i18n.rst
|
||||
|
||||
BIN
docs/images/frontend-app-authn-localhost-preview.png
Normal file
BIN
docs/images/frontend-app-authn-localhost-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 311 KiB |
@@ -10,4 +10,5 @@ module.exports = createConfig('jest', {
|
||||
'src/index.jsx',
|
||||
'MainApp.jsx',
|
||||
],
|
||||
testEnvironment: 'jsdom',
|
||||
});
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
|
||||
nick: Authn MFE
|
||||
oeps: {}
|
||||
owner: edx/vanguards
|
||||
owner: openedx/vanguards
|
||||
openedx-release:
|
||||
ref: master
|
||||
|
||||
43501
package-lock.json
generated
43501
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
92
package.json
92
package.json
@@ -4,16 +4,14 @@
|
||||
"description": "Frontend application template",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/edx/frontend-app-authn.git"
|
||||
"url": "git+https://github.com/openedx/frontend-app-authn.git"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 versions",
|
||||
"ie 11"
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"is-es5": "es-check es5 ./dist/*.js",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
@@ -26,67 +24,63 @@
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
"homepage": "https://github.com/edx/frontend-app-authn#readme",
|
||||
"homepage": "https://github.com/openedx/frontend-app-authn#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/edx/frontend-app-authn/issues"
|
||||
"url": "https://github.com/openedx/frontend-app-authn/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-cookie-policy-banner": "2.1.14",
|
||||
"@edx/frontend-platform": "1.15.5",
|
||||
"@edx/paragon": "19.10.2",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "0.1.18",
|
||||
"classnames": "2.3.1",
|
||||
"clipboard": "2.0.10",
|
||||
"core-js": "3.21.1",
|
||||
"extract-react-intl-messages": "4.1.1",
|
||||
"fastest-levenshtein": "1.0.12",
|
||||
"form-urlencoded": "4.2.1",
|
||||
"formik": "2.2.9",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
||||
"@edx/frontend-platform": "^5.5.4",
|
||||
"@edx/paragon": "20.46.2",
|
||||
"@fortawesome/fontawesome-svg-core": "6.4.2",
|
||||
"@fortawesome/free-brands-svg-icons": "6.4.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.4.2",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@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",
|
||||
"core-js": "3.32.0",
|
||||
"fastest-levenshtein": "1.0.16",
|
||||
"form-urlencoded": "6.1.0",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "5.1.1",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"query-string": "7.1.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-loading-skeleton": "2.2.0",
|
||||
"react-onclickoutside": "6.12.1",
|
||||
"react-redux": "7.2.6",
|
||||
"react-loading-skeleton": "3.3.1",
|
||||
"react-redux": "7.2.9",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-router": "5.2.1",
|
||||
"react-router-dom": "5.3.0",
|
||||
"redux": "4.1.2",
|
||||
"@redux-devtools/extension": "3.2.2",
|
||||
"react-router": "6.15.0",
|
||||
"react-router-dom": "6.15.0",
|
||||
"react-zendesk": "^0.1.13",
|
||||
"redux": "4.2.0",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-mock-store": "1.5.4",
|
||||
"redux-saga": "1.1.3",
|
||||
"redux-thunk": "2.4.1",
|
||||
"regenerator-runtime": "0.13.9",
|
||||
"reselect": "4.1.5",
|
||||
"sanitize-html": "2.7.0",
|
||||
"semver-regex": "3.1.4",
|
||||
"redux-saga": "1.2.3",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "0.14.0",
|
||||
"reselect": "4.1.8",
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "9.1.4",
|
||||
"@edx/reactifex": "1.0.3",
|
||||
"babel-plugin-formatjs": "10.3.18",
|
||||
"codecov": "3.8.2",
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/frontend-build": "12.9.8",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
|
||||
"babel-plugin-formatjs": "10.5.3",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-adapter-react-16": "1.15.6",
|
||||
"es-check": "6.2.1",
|
||||
"glob": "7.2.0",
|
||||
"eslint-plugin-import": "2.28.0",
|
||||
"glob": "7.2.3",
|
||||
"history": "5.3.0",
|
||||
"husky": "7.0.4",
|
||||
"jest": "27.5.1",
|
||||
"react-test-renderer": "16.14.0"
|
||||
"jest": "29.6.2",
|
||||
"react-test-renderer": "^17.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<title>Authn | edX</title>
|
||||
<title>Authn | <%= process.env.SITE_NAME %></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.6/iframeResizer.contentWindow.min.js"
|
||||
integrity="sha512-R7Piufj0/o6jG9ZKrAvS2dblFr2kkuG4XVQwStX+/4P+KwOLUXn2DXy0l1AJDxxqGhkM/FJllZHG2PKOAheYzg=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer">
|
||||
</script>
|
||||
<% if (process.env.OPTIMIZELY_URL) { %>
|
||||
<script
|
||||
src="<%= process.env.OPTIMIZELY_URL %>"
|
||||
@@ -14,58 +19,6 @@
|
||||
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
|
||||
></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>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,44 +1,62 @@
|
||||
import React from 'react';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
UnAuthOnlyRoute, registerIcons, NotFoundPage, Logistration,
|
||||
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk,
|
||||
} from './common-components';
|
||||
import {
|
||||
LOGIN_PAGE, PAGE_NOT_FOUND, REGISTER_PAGE, RESET_PAGE, PASSWORD_RESET_CONFIRM, WELCOME_PAGE,
|
||||
} from './data/constants';
|
||||
import configureStore from './data/configureStore';
|
||||
import {
|
||||
AUTHN_PROGRESSIVE_PROFILING,
|
||||
LOGIN_PAGE,
|
||||
PAGE_NOT_FOUND,
|
||||
PASSWORD_RESET_CONFIRM,
|
||||
RECOMMENDATIONS,
|
||||
REGISTER_EMBEDDED_PAGE,
|
||||
REGISTER_PAGE,
|
||||
RESET_PAGE,
|
||||
} from './data/constants';
|
||||
import { updatePathWithQueryParams } from './data/utils';
|
||||
import ForgotPasswordPage from './forgot-password';
|
||||
import ResetPasswordPage from './reset-password';
|
||||
import WelcomePage, { ProgressiveProfiling } from './welcome';
|
||||
import { ForgotPasswordPage } from './forgot-password';
|
||||
import Logistration from './logistration/Logistration';
|
||||
import { ProgressiveProfiling } from './progressive-profiling';
|
||||
import { RecommendationsPage } from './recommendations';
|
||||
import { RegistrationPage } from './register';
|
||||
import { ResetPasswordPage } from './reset-password';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
registerIcons();
|
||||
|
||||
const MainApp = () => (
|
||||
<AppProvider store={configureStore()}>
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<Redirect to={updatePathWithQueryParams(REGISTER_PAGE)} />
|
||||
</Route>
|
||||
<UnAuthOnlyRoute exact path={LOGIN_PAGE} render={() => <Logistration selectedPage={LOGIN_PAGE} />} />
|
||||
<UnAuthOnlyRoute exact path={REGISTER_PAGE} component={Logistration} />
|
||||
<UnAuthOnlyRoute exact path={RESET_PAGE} component={ForgotPasswordPage} />
|
||||
<Route exact path={PASSWORD_RESET_CONFIRM} component={ResetPasswordPage} />
|
||||
<Helmet>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
{getConfig().ZENDESK_KEY && <Zendesk />}
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
|
||||
<Route
|
||||
exact
|
||||
path={WELCOME_PAGE}
|
||||
component={(getConfig().SHOW_DYNAMIC_PROFILING_PAGE) ? ProgressiveProfiling : WelcomePage}
|
||||
path={REGISTER_EMBEDDED_PAGE}
|
||||
element={<EmbeddedRegistrationRoute><RegistrationPage /></EmbeddedRegistrationRoute>}
|
||||
/>
|
||||
<Route path={PAGE_NOT_FOUND} component={NotFoundPage} />
|
||||
<Route path="*">
|
||||
<Redirect to={PAGE_NOT_FOUND} />
|
||||
</Route>
|
||||
</Switch>
|
||||
<Route
|
||||
path={LOGIN_PAGE}
|
||||
element={
|
||||
<UnAuthOnlyRoute><Logistration selectedPage={LOGIN_PAGE} /></UnAuthOnlyRoute>
|
||||
}
|
||||
/>
|
||||
<Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><Logistration /></UnAuthOnlyRoute>} />
|
||||
<Route path={RESET_PAGE} element={<UnAuthOnlyRoute><ForgotPasswordPage /></UnAuthOnlyRoute>} />
|
||||
<Route path={PASSWORD_RESET_CONFIRM} element={<ResetPasswordPage />} />
|
||||
<Route path={AUTHN_PROGRESSIVE_PROFILING} element={<ProgressiveProfiling />} />
|
||||
<Route path={RECOMMENDATIONS} element={<RecommendationsPage />} />
|
||||
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} />
|
||||
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} />
|
||||
</Routes>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
|
||||
887
src/_style.scss
887
src/_style.scss
@@ -1,887 +0,0 @@
|
||||
// ----------------------------
|
||||
// #COLORS
|
||||
// ----------------------------
|
||||
$font-blue: #126f9a;
|
||||
$white: #FFFFFF;
|
||||
|
||||
// social platforms
|
||||
$facebook-blue: #1877F2;
|
||||
$facebook-focus-blue: #29487d;
|
||||
$google-blue: #4285f4;
|
||||
$google-focus-blue: #287ae6;
|
||||
$microsoft-black: #2f2f2f;
|
||||
$microsoft-focus-black: #000;
|
||||
$apple-black: #000000;
|
||||
$apple-focus-black: $apple-black;
|
||||
$accent-a-light: #c9f2f5;
|
||||
|
||||
.centered-align-spinner {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
@extend .pt-4;
|
||||
min-width: 464px !important;
|
||||
}
|
||||
|
||||
.welcome-page-content {
|
||||
min-width: 464px !important;
|
||||
}
|
||||
|
||||
.stateful-button-width {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.register-stateful-button-width {
|
||||
min-width: 14.4rem;
|
||||
}
|
||||
|
||||
.login-button-width {
|
||||
min-width: 6rem;
|
||||
}
|
||||
|
||||
.tpa-skeleton {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
top: auto;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.close {
|
||||
padding-bottom: 0px !important;
|
||||
padding-top: 3px !important;
|
||||
}
|
||||
|
||||
.focus-out {
|
||||
position: absolute;
|
||||
padding-left: 17px;
|
||||
opacity: 0.75;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.alert-link {
|
||||
font-weight: normal;
|
||||
text-decoration: underline;
|
||||
color: #0075b4 !important;
|
||||
|
||||
&:hover {
|
||||
color: #065683 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.authn-header {
|
||||
border-bottom: 1px solid #e7e7e7;
|
||||
height: 3.75rem;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.authn-header img {
|
||||
height: 1.75rem;
|
||||
margin-left: 2rem;
|
||||
padding: 1rem 0;
|
||||
display: block;
|
||||
position: relative;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
background-color: white !important;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
height: 2.75rem;
|
||||
|
||||
}
|
||||
|
||||
.btn-social {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
margin-bottom: 1rem;
|
||||
font-size: 14px;
|
||||
|
||||
background-color: $white;
|
||||
border: 1px solid $font-blue;
|
||||
width: 224px;
|
||||
height: 36px;
|
||||
color: $font-blue;
|
||||
|
||||
.icon-image {
|
||||
background-color: transparent;
|
||||
max-height: 24px;
|
||||
max-width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-tpa {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding-left: 1rem;
|
||||
width: 14rem;
|
||||
|
||||
.icon-image {
|
||||
background-color: transparent;
|
||||
max-height: 24px;
|
||||
max-width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.font-container {
|
||||
background-color: $font-blue;
|
||||
color: $white;
|
||||
font-size: 11px;
|
||||
|
||||
margin-left: -6px;
|
||||
padding-top: 10px;
|
||||
min-width: 30px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.btn-oa2-facebook {
|
||||
color: $white;
|
||||
border-color: $facebook-blue;
|
||||
background-color: $facebook-blue;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $facebook-focus-blue;
|
||||
border: 1px solid $facebook-focus-blue;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-oa2-google-oauth2 {
|
||||
color: $white;
|
||||
border-color: $google-blue;
|
||||
background-color: $google-blue;
|
||||
|
||||
.icon-image {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $google-focus-blue;
|
||||
border: 1px solid $google-focus-blue;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-oa2-apple-id {
|
||||
color: $white;
|
||||
border-color: $apple-black;
|
||||
background-color: $apple-black;
|
||||
font-size: 16px;
|
||||
|
||||
.icon-image {
|
||||
max-height: 1.8em;
|
||||
max-width: 2.0em;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $apple-focus-black;
|
||||
border: 1px solid $apple-focus-black;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-oa2-azuread-oauth2 {
|
||||
color: $white;
|
||||
border-color: $microsoft-black;
|
||||
background-color: $microsoft-black;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $microsoft-focus-black;
|
||||
border: 1px solid $microsoft-focus-black;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.submit {
|
||||
display: inherit;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.institute-icon {
|
||||
@extend .mr-1;
|
||||
@extend .text-gray;
|
||||
|
||||
display: inline-block;
|
||||
margin-bottom: 0.25rem;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
|
||||
svg {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.login-help {
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
.full-vertical-height {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.help-links {
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
select.form-control {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
#honor-code p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#honor-code a span {
|
||||
@extend .sr-only;
|
||||
}
|
||||
|
||||
.font-weight-500 {
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.mw-420 {
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.mw-500 {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.mw-32em {
|
||||
max-width: 32em;
|
||||
}
|
||||
|
||||
.h-90 {
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mt-7 {
|
||||
margin-top: 7rem;
|
||||
}
|
||||
|
||||
.pt-10 {
|
||||
padding-top: 10px;
|
||||
}
|
||||
.tooltip-shadow {
|
||||
box-shadow: 0px 5px 15px 0px rgba(0,0,0,0.3) !important;
|
||||
}
|
||||
#password-requirement-left {
|
||||
opacity: 1;
|
||||
.tooltip-inner {
|
||||
@extend .tooltip-shadow;
|
||||
background: white;
|
||||
display: block;
|
||||
color: #707070;
|
||||
}
|
||||
.arrow::before {
|
||||
border-left-color: #fff;
|
||||
@extend .tooltip-shadow;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#password-requirement-top {
|
||||
opacity: 1;
|
||||
width: 90%;
|
||||
margin-bottom: 10px;
|
||||
.tooltip-inner {
|
||||
max-width: inherit;
|
||||
background: white;
|
||||
display: block;
|
||||
@extend .tooltip-shadow;
|
||||
color: #707070;
|
||||
}
|
||||
.arrow::before {
|
||||
border-top-color: #fff;
|
||||
@extend .tooltip-shadow;
|
||||
}
|
||||
|
||||
}
|
||||
#forgotpassword-success-alert {
|
||||
.alert-link {
|
||||
color: #454545 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.screen-header-light {
|
||||
background-color: $light-200;
|
||||
}
|
||||
|
||||
.screen-header-primary {
|
||||
background-color: $primary-400;
|
||||
}
|
||||
|
||||
.large-screen-container {
|
||||
background-color: $white;
|
||||
width: 50vw;
|
||||
}
|
||||
|
||||
.large-screen-svg-light,
|
||||
.large-screen-svg-primary {
|
||||
fill: $light-200;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.large-screen-svg-primary {
|
||||
fill: $primary-400;
|
||||
}
|
||||
|
||||
.medium-screen-container {
|
||||
flex-wrap: nowrap;
|
||||
max-width: inherit;
|
||||
height: 282px;
|
||||
}
|
||||
|
||||
.variation1-medium-screen {
|
||||
height: 300px !important;
|
||||
}
|
||||
|
||||
.medium-screen-svg-light,
|
||||
.medium-screen-svg-primary {
|
||||
fill: $light-200;
|
||||
overflow: inherit;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.medium-screen-svg-primary {
|
||||
fill: $primary-400;
|
||||
}
|
||||
|
||||
.screen-polygon {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.small-screen-header-light,
|
||||
.small-screen-header-primary {
|
||||
background-color: $light-200;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.small-screen-header-primary {
|
||||
background-color: $primary-400;
|
||||
}
|
||||
|
||||
.extra-large-screen-top-stripe {
|
||||
height: 0.5rem;
|
||||
background-image: linear-gradient(
|
||||
102.02deg,
|
||||
$brand-700 10%,
|
||||
$brand 10%,
|
||||
$brand 45%,
|
||||
$primary-700 45%,
|
||||
$primary-700 55%,
|
||||
$accent-a 55%,
|
||||
$accent-a 75%,
|
||||
$accent-a-light 75%,
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.medium-screen-top-stripe {
|
||||
height: 0.5rem;
|
||||
background-image: linear-gradient(
|
||||
102.02deg,
|
||||
$brand-700,
|
||||
$brand-700 10%,
|
||||
$brand 10%,
|
||||
$brand 90%,
|
||||
$primary-700 90%,
|
||||
$primary-700 100%,
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.large-screen-top-stripe {
|
||||
height: 0.5rem;
|
||||
background-image: linear-gradient(
|
||||
102.02deg,
|
||||
$brand-700 10%,
|
||||
$brand 10%,
|
||||
$brand 65%,
|
||||
$primary-700 65%,
|
||||
$primary-700 75%,
|
||||
$accent-a 75%,
|
||||
$accent-a 75%);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.small-screen-top-stripe {
|
||||
height: 0.25rem;
|
||||
background-image: linear-gradient(
|
||||
102.02deg,
|
||||
$brand-700,
|
||||
$brand-700 20%,
|
||||
$brand 20%,
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
// Progressive profiling base component classes
|
||||
.medium-container {
|
||||
flex-wrap: nowrap;
|
||||
max-width: inherit;
|
||||
height: 260px;
|
||||
}
|
||||
|
||||
.extra-extra-large-svg-line {
|
||||
stroke: $accent-b;
|
||||
stroke-width: 0.5rem;
|
||||
|
||||
width: 5.5rem;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.extra-large-svg-line {
|
||||
stroke: $accent-b;
|
||||
stroke-width: 0.5rem;
|
||||
|
||||
width: 5.5rem;
|
||||
height: 110px;
|
||||
}
|
||||
|
||||
.medium-svg-line {
|
||||
stroke: $accent-b;
|
||||
stroke-width: 0.5rem;
|
||||
width: 7em;
|
||||
height: 110px;
|
||||
}
|
||||
|
||||
.small-svg-line {
|
||||
stroke: $accent-b;
|
||||
stroke-width: 0.25rem;
|
||||
|
||||
width: 4em;
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
.extra-small-svg-line {
|
||||
stroke: $accent-b;
|
||||
stroke-width: 0.25rem;
|
||||
|
||||
width: 4em;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
// Non-Auth Screen Svg Lines
|
||||
.large-screen-svg-line {
|
||||
padding-top: 0.5rem;
|
||||
stroke: $accent-b;
|
||||
stroke-width: 0.5rem;
|
||||
width: 5.5rem;
|
||||
height: 240px;
|
||||
}
|
||||
|
||||
.variation1-bar-color {
|
||||
stroke: $brand !important;
|
||||
}
|
||||
|
||||
.variation2-bar-color {
|
||||
stroke: $accent-a !important;
|
||||
}
|
||||
|
||||
.medium-screen-svg-line {
|
||||
padding-top: 0.5rem;
|
||||
stroke: $accent-b;
|
||||
stroke-width: 0.5rem;
|
||||
width: 7em;
|
||||
height: 115px;
|
||||
}
|
||||
|
||||
.small-screen-svg-line {
|
||||
padding-top: 0.5rem;
|
||||
stroke: $accent-b;
|
||||
stroke-width: 0.25rem;
|
||||
width: 4em;
|
||||
height: 72px;
|
||||
}
|
||||
.dicount-heading{
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
.hover-text:hover {
|
||||
color: $black !important;
|
||||
|
||||
.hover-icon {
|
||||
color: $black !important;
|
||||
}
|
||||
}
|
||||
|
||||
.hover-discount-icon:hover {
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
.large-heading {
|
||||
margin-left: 7px;
|
||||
color: $white;
|
||||
max-width: 24.5rem;
|
||||
line-height: 78px;
|
||||
font-size: 78px;
|
||||
}
|
||||
|
||||
.medium-heading {
|
||||
padding-left: 1rem;
|
||||
color: $white;
|
||||
max-width: 27rem;
|
||||
line-height: 60px;
|
||||
font-size: 60px;
|
||||
}
|
||||
|
||||
.small-heading {
|
||||
padding-left: 0.5rem;
|
||||
color: $white;
|
||||
line-height: 40px;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 4.44rem;
|
||||
margin-top: 1.5rem;
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
.username-suggestion {
|
||||
padding: 1px 0.5rem;
|
||||
margin: 0.25rem;
|
||||
border-radius: 0.375rem;
|
||||
line-height: 24px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
color: $primary-700;
|
||||
}
|
||||
|
||||
.username-suggestion-label {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.25rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.yellow-border {
|
||||
border: 2px solid #F0CC00;
|
||||
}
|
||||
|
||||
.one-rem-font {
|
||||
font-size: 0.99rem;
|
||||
color: #707070;
|
||||
}
|
||||
|
||||
.institute-heading {
|
||||
color: $primary-700;
|
||||
}
|
||||
|
||||
.logistration-button {
|
||||
color: $gray-700;
|
||||
}
|
||||
|
||||
.logistration-button:hover{
|
||||
color: $gray-700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dropdown-item:active {
|
||||
background-color: #F2F0EF;
|
||||
}
|
||||
|
||||
.dropdown-container {
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.15), 0px 2px 8px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 4px;
|
||||
max-height: 200px;
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
line-height: 1.25rem;
|
||||
overflow-y: scroll;
|
||||
position: absolute;
|
||||
background-color: #fff;
|
||||
width: 464px;
|
||||
z-index: 100 !important;
|
||||
}
|
||||
|
||||
.email-error-alert {
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
.alert-close {
|
||||
float: right;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-text {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.alert-link {
|
||||
color: $primary !important;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: $info-700 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.email-warning-alert-link {
|
||||
color: $info-500 !important;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: $info-700 !important;
|
||||
}
|
||||
}
|
||||
|
||||
#forgot-password {
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow-back-icon {
|
||||
margin-top:2px;
|
||||
}
|
||||
|
||||
.icon-size {
|
||||
width: 2.3rem;
|
||||
}
|
||||
.has-floating-label {
|
||||
color: $gray-500;
|
||||
}
|
||||
.pgn__form-control-floating-label .pgn__form-control-floating-label-content {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.pgn__form-text {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.form-field .form-control:focus ~ .pgn__form-control-floating-label .pgn__form-control-floating-label-content {
|
||||
font-size: 16px;
|
||||
color: $primary-700;
|
||||
}
|
||||
|
||||
.form-field .form-control:not([value='']):not(:focus) ~
|
||||
.pgn__form-control-floating-label .pgn__form-control-floating-label-content {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.pgn__form-group {
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
|
||||
.form-text-size {
|
||||
margin-top: 0.188rem !important;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.progressive-profiling-support {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.discount-banner {
|
||||
background-color: #03C7E8;
|
||||
}
|
||||
|
||||
.dashed-border {
|
||||
border-style: dashed;
|
||||
border-width: thin;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.mw-500 {
|
||||
width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.form-control {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 463px) {
|
||||
.reset-password-container {
|
||||
width: 420px;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.tpa-skeleton {
|
||||
min-width: 464px !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.welcome-page-content {
|
||||
padding-top: 1.5rem !important;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction:column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dashed-border {
|
||||
border-style: dashed;
|
||||
border-width: thin;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1199px) and (min-width: 768px) {
|
||||
.layout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction:column;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.layout{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
}
|
||||
.content {
|
||||
width: 50vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 464px) {
|
||||
.dropdown-container {
|
||||
width: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 500px) {
|
||||
.large-screen-svg-line, .large-heading {
|
||||
margin-top: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.alert {
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 960px) {
|
||||
html {
|
||||
margin-right: calc(100% - 100vw);
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.variation2-text-alignment {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
// Smaller than Extra Small (Mobile Screens)
|
||||
@media (max-width: 464px) {
|
||||
.btn-social {
|
||||
min-width: 100%;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.welcome-page-content,
|
||||
.main-content {
|
||||
min-width: 100vw !important;
|
||||
padding: 1.5rem !important;
|
||||
}
|
||||
|
||||
.progressive-profiling-support {
|
||||
font-size: 0.688rem;
|
||||
font-weight: normal;
|
||||
line-height: 0.938rem;
|
||||
}
|
||||
}
|
||||
|
||||
.table-striped tbody tr:nth-of-type(odd) {
|
||||
background-color: $light-200;
|
||||
}
|
||||
|
||||
.secondary-provider-link {
|
||||
font-weight: normal;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5rem;
|
||||
color: $primary-700
|
||||
}
|
||||
|
||||
.opt-checkbox {
|
||||
.pgn__form-label {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
margin-left: 3px;
|
||||
}
|
||||
.suggested-username {
|
||||
position: relative;
|
||||
margin-top: -8.7%;
|
||||
margin-left: 15px;
|
||||
}
|
||||
.suggested-username-close-button {
|
||||
right: 1rem;
|
||||
position: absolute;
|
||||
}
|
||||
.suggested-username-with-error {
|
||||
position: relative;
|
||||
margin-top: -13.7%;
|
||||
margin-bottom: 11%;
|
||||
margin-left: 15px;
|
||||
}
|
||||
.scroll-suggested-username {
|
||||
width: 21rem;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.pgn__form-control-decorator-trailing {
|
||||
right: 0 !important;
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Col, Hyperlink, Image, Row,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const AuthExtraLargeLayout = (props) => {
|
||||
const { intl, username, variant } = props;
|
||||
|
||||
return (
|
||||
<div className="container row p-0 m-0 large-screen-container">
|
||||
<div className="col-md-9 p-0 screen-header-light">
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image alt={getConfig().SITE_NAME} className="logo position-absolute" src={getConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="min-vh-100 d-flex align-items-center">
|
||||
<div>
|
||||
<Row>
|
||||
<Col xs={3}>
|
||||
<svg className={classNames(
|
||||
'ml-5 mt-5',
|
||||
{
|
||||
'extra-large-svg-line': variant === 'xl',
|
||||
'extra-extra-large-svg-line': variant === 'xxl',
|
||||
},
|
||||
)}
|
||||
>
|
||||
<line x1="60" y1="0" x2="5" y2="220" />
|
||||
</svg>
|
||||
</Col>
|
||||
<Col xs={9}>
|
||||
<div className={classNames(
|
||||
'data-hj-suppress',
|
||||
{
|
||||
h3: variant === 'xl',
|
||||
h2: variant === 'xxl',
|
||||
},
|
||||
)}
|
||||
>
|
||||
{intl.formatMessage(
|
||||
messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username },
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'text-primary',
|
||||
{
|
||||
'display-1': variant === 'xl',
|
||||
'display-2': variant === 'xxl',
|
||||
},
|
||||
)}
|
||||
>
|
||||
{intl.formatMessage(messages['complete.your.profile.1'])}
|
||||
<span className="text-accent-a">
|
||||
<br />
|
||||
{intl.formatMessage(messages['complete.your.profile.2'])}
|
||||
</span>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-3 p-0 screen-polygon">
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
className="m1-n1 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>
|
||||
);
|
||||
};
|
||||
|
||||
AuthExtraLargeLayout.defaultProps = {
|
||||
variant: 'xl',
|
||||
};
|
||||
|
||||
AuthExtraLargeLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
variant: PropTypes.oneOf(['xl', 'xxl']),
|
||||
};
|
||||
|
||||
export default injectIntl(AuthExtraLargeLayout);
|
||||
@@ -1,69 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
Col, Hyperlink, Image, Row,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const AuthMediumLayout = (props) => {
|
||||
const { intl, username } = props;
|
||||
|
||||
return (
|
||||
<div className="container row p-0 mb-3 medium-container">
|
||||
<div className="col-md-10 p-0 screen-header-light">
|
||||
<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 ml-6">
|
||||
<div>
|
||||
<Row>
|
||||
<Col xs={3}>
|
||||
<svg className="medium-svg-line ml-5 mt-5">
|
||||
<line x1="60" y1="0" x2="5" y2="220" />
|
||||
</svg>
|
||||
</Col>
|
||||
<Col xs={9}>
|
||||
<h3 className="data-hj-suppress">
|
||||
{intl.formatMessage(
|
||||
messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username },
|
||||
)}
|
||||
</h3>
|
||||
<div className="display-1 text-primary">
|
||||
{intl.formatMessage(messages['complete.your.profile.1'])}
|
||||
<span className="text-accent-a">
|
||||
<br />
|
||||
{intl.formatMessage(messages['complete.your.profile.2'])}
|
||||
</span>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2 p-0 screen-polygon">
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
className="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,68 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Col, Hyperlink, Image, Row,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const AuthSmallLayout = (props) => {
|
||||
const { intl, username, variant } = props;
|
||||
|
||||
return (
|
||||
<div className="small-screen-header-light">
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image alt={getConfig().SITE_NAME} className="logo" src={getConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className={classNames('d-flex mt-3', { 'pl-6': variant === 'sm' })}>
|
||||
<div>
|
||||
<Row>
|
||||
<Col xs={3}>
|
||||
<svg className={classNames(
|
||||
'mt-4\.5', // eslint-disable-line no-useless-escape
|
||||
{
|
||||
'extra-small-svg-line': variant === 'xs',
|
||||
'small-svg-line': variant === 'sm',
|
||||
},
|
||||
)}
|
||||
>
|
||||
<line x1="60" y1="0" x2="5" y2="220" />
|
||||
</svg>
|
||||
</Col>
|
||||
<Col xs={9}>
|
||||
<h5 className="data-hj-suppress">
|
||||
{intl.formatMessage(
|
||||
messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username },
|
||||
)}
|
||||
</h5>
|
||||
<h1>
|
||||
{intl.formatMessage(messages['complete.your.profile.1'])}
|
||||
<br />
|
||||
<span className="text-accent-a">
|
||||
{intl.formatMessage(messages['complete.your.profile.2'])}
|
||||
</span>
|
||||
</h1>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AuthSmallLayout.defaultProps = {
|
||||
variant: 'sm',
|
||||
};
|
||||
|
||||
AuthSmallLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
variant: PropTypes.oneOf(['sm', 'xs']),
|
||||
};
|
||||
|
||||
export default injectIntl(AuthSmallLayout);
|
||||
@@ -1,98 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import MediaQuery from 'react-responsive';
|
||||
import { breakpoints } from '@edx/paragon';
|
||||
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
|
||||
import { getLocale } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import LargeLayout from './LargeLayout';
|
||||
import MediumLayout from './MediumLayout';
|
||||
import SmallLayout from './SmallLayout';
|
||||
|
||||
import AuthExtraLargeLayout from './AuthExtraLargeLayout';
|
||||
import AuthMediumLayout from './AuthMediumLayout';
|
||||
import AuthSmallLayout from './AuthSmallLayout';
|
||||
import DiscountExperimentBanner from './DiscountBanner';
|
||||
|
||||
const BaseComponent = ({ children, isRegistrationPage, showWelcomeBanner }) => {
|
||||
const authenticatedUser = showWelcomeBanner ? getAuthenticatedUser() : null;
|
||||
const [optimizelyExperimentName, setOptimizelyExperimentName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const { experimentName } = window;
|
||||
|
||||
if (experimentName) {
|
||||
setOptimizelyExperimentName(experimentName);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{isRegistrationPage && optimizelyExperimentName === 'variation2' ? <DiscountExperimentBanner /> : null}
|
||||
<CookiePolicyBanner languageCode={getLocale()} />
|
||||
<MediaQuery minWidth={breakpoints.extraLarge.minWidth} maxWidth={breakpoints.extraLarge.maxWidth}>
|
||||
<div className="col-md-12 extra-large-screen-top-stripe" />
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.extraExtraLarge.minWidth} maxWidth={breakpoints.extraExtraLarge.maxWidth}>
|
||||
<div className="col-md-12 extra-large-screen-top-stripe" />
|
||||
</MediaQuery>
|
||||
|
||||
<div className={classNames('layout', { authenticated: authenticatedUser })}>
|
||||
<MediaQuery maxWidth={breakpoints.extraSmall.maxWidth}>
|
||||
<div className="col-md-12 small-screen-top-stripe" />
|
||||
{authenticatedUser ? <AuthSmallLayout variant="xs" username={authenticatedUser.username} /> : (
|
||||
<SmallLayout experimentName={optimizelyExperimentName} isRegistrationPage={isRegistrationPage} />
|
||||
)}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.small.minWidth} maxWidth={breakpoints.small.maxWidth}>
|
||||
<div className="col-md-12 small-screen-top-stripe" />
|
||||
{authenticatedUser ? <AuthSmallLayout username={authenticatedUser.username} /> : (
|
||||
<SmallLayout experimentName={optimizelyExperimentName} isRegistrationPage={isRegistrationPage} />
|
||||
)}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.medium.maxWidth}>
|
||||
<div className="w-100 medium-screen-top-stripe" />
|
||||
{authenticatedUser ? <AuthMediumLayout username={authenticatedUser.username} /> : (
|
||||
<MediumLayout experimentName={optimizelyExperimentName} isRegistrationPage={isRegistrationPage} />
|
||||
)}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.large.minWidth} maxWidth={breakpoints.large.maxWidth}>
|
||||
<div className="w-100 large-screen-top-stripe" />
|
||||
{authenticatedUser ? <AuthMediumLayout username={authenticatedUser.username} /> : (
|
||||
<MediumLayout experimentName={optimizelyExperimentName} isRegistrationPage={isRegistrationPage} />
|
||||
)}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.extraLarge.minWidth} maxWidth={breakpoints.extraLarge.maxWidth}>
|
||||
{authenticatedUser ? <AuthExtraLargeLayout username={authenticatedUser.username} /> : (
|
||||
<LargeLayout experimentName={optimizelyExperimentName} isRegistrationPage={isRegistrationPage} />
|
||||
)}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.extraExtraLarge.minWidth} maxWidth={breakpoints.extraExtraLarge.maxWidth}>
|
||||
{authenticatedUser ? <AuthExtraLargeLayout variant="xxl" username={authenticatedUser.username} /> : (
|
||||
<LargeLayout experimentName={optimizelyExperimentName} isRegistrationPage={isRegistrationPage} />
|
||||
)}
|
||||
</MediaQuery>
|
||||
|
||||
<div className={classNames('content', { 'align-items-center mt-0': authenticatedUser })}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
BaseComponent.defaultProps = {
|
||||
isRegistrationPage: false,
|
||||
showWelcomeBanner: false,
|
||||
};
|
||||
|
||||
BaseComponent.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
isRegistrationPage: PropTypes.bool,
|
||||
showWelcomeBanner: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default BaseComponent;
|
||||
@@ -1,71 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import ClipboardJS from 'clipboard';
|
||||
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { faCut } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { Toast, PageBanner } from '@edx/paragon';
|
||||
import messages from './messages';
|
||||
|
||||
const DiscountExperimentBanner = (props) => {
|
||||
const { intl } = props;
|
||||
const [show, setShow] = useState(true);
|
||||
const [showToast, setToastShow] = useState(false);
|
||||
new ClipboardJS('.copyIcon'); // eslint-disable-line no-new
|
||||
const getDiscountText = () => (
|
||||
<strong>
|
||||
15% <FormattedMessage
|
||||
id="top.discount.message.15.off"
|
||||
defaultMessage="off"
|
||||
description="Text used with discounts e.g. 15% off"
|
||||
/>
|
||||
</strong>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toast
|
||||
onClose={() => setToastShow(false)}
|
||||
show={showToast}
|
||||
>
|
||||
{intl.formatMessage(messages['code.copied'])}
|
||||
</Toast>
|
||||
<PageBanner
|
||||
show={show}
|
||||
dismissible
|
||||
onDismiss={() => { setShow(false); }}
|
||||
>
|
||||
<span className="text-primary-700 small variation2-text-alignment">
|
||||
<span className="mr-3">
|
||||
<FormattedMessage
|
||||
id="top.discount.message.body"
|
||||
defaultMessage="Get {discount} your first verified certificate* with code"
|
||||
description="Message body for edX discount"
|
||||
values={{
|
||||
discount: getDiscountText(),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span className="hover-text dashed-border p-1 d-inline-flex flex-wrap align-items-center">
|
||||
<span id="edx-welcome" className="font-weight-bold ">EDXWELCOME</span>
|
||||
<FontAwesomeIcon
|
||||
className="text-dark-200 copyIcon ml-2 hover-icon"
|
||||
icon={faCut}
|
||||
data-clipboard-action="copy"
|
||||
data-clipboard-target="#edx-welcome"
|
||||
onClick={() => setToastShow(true)}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</PageBanner>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
DiscountExperimentBanner.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
};
|
||||
export default injectIntl(DiscountExperimentBanner);
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
|
||||
import LargeScreenLeftLayout from './LargeLeftLayout';
|
||||
|
||||
const LargeLayout = ({ experimentName, isRegistrationPage }) => (
|
||||
<div className="container row p-0 m-0 large-screen-container">
|
||||
<div className="col-md-9 p-0 screen-header-primary">
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image alt={getConfig().SITE_NAME} className="logo position-absolute" src={getConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<LargeScreenLeftLayout experimentName={experimentName} isRegistrationPage={isRegistrationPage} />
|
||||
</div>
|
||||
<div className="col-md-3 p-0 screen-polygon">
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
className="ml-n1 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.defaultProps = {
|
||||
experimentName: '',
|
||||
isRegistrationPage: false,
|
||||
};
|
||||
|
||||
LargeLayout.propTypes = {
|
||||
experimentName: PropTypes.string,
|
||||
isRegistrationPage: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default LargeLayout;
|
||||
@@ -1,86 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import PropTypes from 'prop-types';
|
||||
import ClipboardJS from 'clipboard';
|
||||
|
||||
import { faCut } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Toast } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
import SideDiscountBanner from './SideDiscountBanner';
|
||||
|
||||
const LargeLeftLayout = (props) => {
|
||||
const { intl, isRegistrationPage, experimentName } = props;
|
||||
const [showToast, setToastShow] = useState(false);
|
||||
new ClipboardJS('.copyIcon'); // eslint-disable-line no-new
|
||||
|
||||
return (
|
||||
<div className="min-vh-100 d-flex justify-content-left align-items-center">
|
||||
<div className="d-flex pr-0 mt-lg-n2">
|
||||
<Toast
|
||||
onClose={() => setToastShow(false)}
|
||||
show={showToast}
|
||||
>
|
||||
{intl.formatMessage(messages['code.copied'])}
|
||||
</Toast>
|
||||
<svg className={classNames(
|
||||
'large-screen-svg-line',
|
||||
{
|
||||
'variation1-bar-color mt-n6 pt-0 ml-5': experimentName === 'variation1' && isRegistrationPage,
|
||||
'variation2-bar-color': experimentName === 'variation2' && isRegistrationPage,
|
||||
'ml-5': experimentName !== 'variation1' || !isRegistrationPage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<line x1="50" y1="0" x2="10" y2="215" />
|
||||
</svg>
|
||||
<div className={classNames({ 'pl-4': experimentName === 'variation1' && isRegistrationPage })}>
|
||||
<h1 className={classNames('large-heading', { 'mb-4.5': experimentName === 'variation1' && isRegistrationPage })}>
|
||||
{intl.formatMessage(messages['start.learning'])}
|
||||
<span
|
||||
className={((experimentName === 'variation1' || experimentName === 'variation2') && isRegistrationPage) ? 'text-accent-b' : 'text-accent-a'}
|
||||
>
|
||||
<br />
|
||||
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</span>
|
||||
</h1>
|
||||
{experimentName === 'variation1' && isRegistrationPage ? (
|
||||
<span className="text-light-300 dicount-heading">
|
||||
<span className="lead mr-3">
|
||||
<SideDiscountBanner />
|
||||
</span>
|
||||
<span className="dashed-border d-inline-flex flex-wrap align-items-center">
|
||||
<span id="edx-welcome" className="text-white edx-welcome font-weight-bold mr-1">EDXWELCOME</span>
|
||||
<FontAwesomeIcon
|
||||
className="text-light-700 copyIcon ml-1.5 hover-discount-icon"
|
||||
icon={faCut}
|
||||
data-clipboard-action="copy"
|
||||
data-clipboard-target="#edx-welcome"
|
||||
onClick={() => setToastShow(true)}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LargeLeftLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
experimentName: PropTypes.string,
|
||||
isRegistrationPage: PropTypes.bool,
|
||||
};
|
||||
|
||||
LargeLeftLayout.defaultProps = {
|
||||
experimentName: '',
|
||||
isRegistrationPage: false,
|
||||
};
|
||||
|
||||
export default injectIntl(LargeLeftLayout);
|
||||
@@ -1,102 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Hyperlink, Image, Toast } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ClipboardJS from 'clipboard';
|
||||
|
||||
import { faCut } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import messages from './messages';
|
||||
import SideDiscountBanner from './SideDiscountBanner';
|
||||
|
||||
const MediumLayout = (props) => {
|
||||
const { intl, isRegistrationPage, experimentName } = props;
|
||||
const [showToast, setToastShow] = useState(false);
|
||||
new ClipboardJS('.copyIcon'); // eslint-disable-line no-new
|
||||
|
||||
return (
|
||||
<div className={classNames(
|
||||
'container row p-0 mb-3 medium-screen-container',
|
||||
{
|
||||
'variation1-medium-screen': experimentName === 'variation1' && isRegistrationPage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Toast
|
||||
onClose={() => setToastShow(false)}
|
||||
show={showToast}
|
||||
>
|
||||
{intl.formatMessage(messages['code.copied'])}
|
||||
</Toast>
|
||||
<div className="col-md-10 p-0 screen-header-primary">
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image alt={getConfig().SITE_NAME} className="logo" src={getConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="row mt-4 justify-content-center">
|
||||
<svg className={classNames(
|
||||
'medium-screen-svg-line pl-5',
|
||||
{
|
||||
'variation1-bar-color': experimentName === 'variation1' && isRegistrationPage,
|
||||
'variation2-bar-color': experimentName === 'variation2' && isRegistrationPage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<line x1="50" y1="0" x2="10" y2="215" />
|
||||
</svg>
|
||||
<div className="pb-4">
|
||||
<h1 className="medium-heading">
|
||||
{intl.formatMessage(messages['start.learning'])}
|
||||
<span
|
||||
className={((experimentName === 'variation1' || experimentName === 'variation2') && isRegistrationPage) ? 'text-accent-b' : 'text-accent-a'}
|
||||
>
|
||||
<br />
|
||||
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</span>
|
||||
</h1>
|
||||
{experimentName === 'variation1' && isRegistrationPage ? (
|
||||
<div className="text-light-300 pl-3">
|
||||
<SideDiscountBanner />
|
||||
<span className="dashed-border h5 text-white">
|
||||
<span id="edx-welcome" className="edx-welcome">EDXWELCOME </span>
|
||||
<FontAwesomeIcon
|
||||
className="text-light-700 copyIcon ml-1 hover-discount-icon"
|
||||
icon={faCut}
|
||||
data-clipboard-action="copy"
|
||||
data-clipboard-target="#edx-welcome"
|
||||
onClick={() => setToastShow(true)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
<div className="col-md-2 p-0 screen-polygon">
|
||||
<svg width="100%" height="100%" className="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,
|
||||
experimentName: PropTypes.string,
|
||||
isRegistrationPage: PropTypes.bool,
|
||||
};
|
||||
|
||||
MediumLayout.defaultProps = {
|
||||
experimentName: '',
|
||||
isRegistrationPage: false,
|
||||
};
|
||||
|
||||
export default injectIntl(MediumLayout);
|
||||
@@ -1,38 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export default function SideDiscountBanner() {
|
||||
const getDiscountText = () => (
|
||||
<span className="text-accent-a h3">
|
||||
15% <FormattedMessage
|
||||
id="side.discount.message.15.off"
|
||||
defaultMessage="off"
|
||||
description="Text used with discounts e.g. 15% off"
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
const getCerificateMsg = () => (
|
||||
<span className="dicount-heading">
|
||||
<FormattedMessage
|
||||
id="certificate.message"
|
||||
defaultMessage="certificate* with code"
|
||||
description="Text with certificate"
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<span className="mr-1.5">
|
||||
<FormattedMessage
|
||||
id="side.discount.message.body"
|
||||
defaultMessage="Get {discountText} your first verified {lineBreak} {certificateMsg}"
|
||||
description="Message body for edX discount"
|
||||
values={{
|
||||
discountText: getDiscountText(),
|
||||
lineBreak: <br />,
|
||||
certificateMsg: getCerificateMsg(),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Hyperlink, Image, Toast } from '@edx/paragon';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ClipboardJS from 'clipboard';
|
||||
|
||||
import { faCut } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import messages from './messages';
|
||||
import SideDiscountBanner from './SideDiscountBanner';
|
||||
|
||||
const SmallLayout = (props) => {
|
||||
const { intl, isRegistrationPage, experimentName } = props;
|
||||
const [showToast, setToastShow] = useState(false);
|
||||
new ClipboardJS('.copyIcon'); // eslint-disable-line no-new
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="small-screen-header-primary">
|
||||
<Toast
|
||||
onClose={() => setToastShow(false)}
|
||||
show={showToast}
|
||||
>
|
||||
{intl.formatMessage(messages['code.copied'])}
|
||||
</Toast>
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image alt={getConfig().SITE_NAME} className="logo" src={getConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="d-flex mt-3">
|
||||
<svg className={classNames(
|
||||
'small-screen-svg-line',
|
||||
{
|
||||
'variation1-bar-color': experimentName === 'variation1' && isRegistrationPage,
|
||||
'variation2-bar-color': experimentName === 'variation2' && isRegistrationPage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<line x1="55" y1="0" x2="40" y2="65" />
|
||||
</svg>
|
||||
<div className="pb-3">
|
||||
<h1 className="small-heading">
|
||||
{intl.formatMessage(messages['start.learning'])}
|
||||
<br />
|
||||
<span
|
||||
className={((experimentName === 'variation1' || experimentName === 'variation2') && isRegistrationPage) ? 'text-accent-b' : 'text-accent-a'}
|
||||
>
|
||||
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</span>
|
||||
</h1>
|
||||
{(experimentName === 'variation1' && isRegistrationPage) ? (
|
||||
<div className="small text-light-300 pl-2">
|
||||
<SideDiscountBanner />
|
||||
<span className="dashed-border h6 text-white d-inline-flex flex-wrap align-items-center">
|
||||
<span id="edx-welcome" className="edx-welcome mr-1">EDXWELCOME</span>
|
||||
<FontAwesomeIcon
|
||||
className="text-light-700 copyIcon ml-1 hover-discount-icon"
|
||||
icon={faCut}
|
||||
data-clipboard-action="copy"
|
||||
data-clipboard-target="#edx-welcome"
|
||||
onClick={() => setToastShow(true)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
SmallLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
experimentName: PropTypes.string,
|
||||
isRegistrationPage: PropTypes.bool,
|
||||
};
|
||||
|
||||
SmallLayout.defaultProps = {
|
||||
experimentName: '',
|
||||
isRegistrationPage: false,
|
||||
|
||||
};
|
||||
|
||||
export default injectIntl(SmallLayout);
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './BaseComponent';
|
||||
@@ -1,17 +1,16 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import LargeLayout from '../LargeLayout';
|
||||
import MediumLayout from '../MediumLayout';
|
||||
import SmallLayout from '../SmallLayout';
|
||||
import { DefaultLargeLayout, DefaultMediumLayout, DefaultSmallLayout } from './index';
|
||||
|
||||
describe('ScreenLayout', () => {
|
||||
it('should display the form, pass as a child in SmallScreenLayout', () => {
|
||||
describe('Default Layout tests', () => {
|
||||
it('should display the form passed as a child in SmallScreenLayout', () => {
|
||||
const smallScreen = mount(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<SmallLayout />
|
||||
<DefaultSmallLayout />
|
||||
<form>
|
||||
<input type="text" />
|
||||
</form>
|
||||
@@ -21,11 +20,11 @@ describe('ScreenLayout', () => {
|
||||
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(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<MediumLayout />
|
||||
<DefaultMediumLayout />
|
||||
<form>
|
||||
<input type="text" />
|
||||
</form>
|
||||
@@ -35,11 +34,11 @@ describe('ScreenLayout', () => {
|
||||
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(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<LargeLayout />
|
||||
<DefaultLargeLayout />
|
||||
<form>
|
||||
<input type="text" />
|
||||
</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,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 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 main-heading',
|
||||
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' },
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
{formatMessage(messages['start.learning'])}{' '}
|
||||
<span className="text-accent-a d-inline-block">
|
||||
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</span>
|
||||
</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;
|
||||
40
src/base-container/components/default-layout/SmallLayout.jsx
Normal file
40
src/base-container/components/default-layout/SmallLayout.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
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 m-3.5">
|
||||
<div className={classNames({ 'small-yellow-line mr-n2.5': getConfig().SITE_NAME === 'edX' })} />
|
||||
<h1
|
||||
className={classNames(
|
||||
'text-white mt-3.5 mb-3.5',
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
{formatMessage(messages['start.learning'])}{' '}
|
||||
<span className="text-accent-a d-inline-block">
|
||||
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</span>
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
</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 m-3.5">
|
||||
<div className="small-yellow-line mt-4.5" />
|
||||
<div>
|
||||
<h1 className="h5 data-hj-suppress">
|
||||
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
|
||||
</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,22 +1,11 @@
|
||||
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',
|
||||
'welcome.to.platform': {
|
||||
id: 'welcome.to.platform',
|
||||
defaultMessage: 'Welcome to {siteName}, {username}!',
|
||||
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',
|
||||
},
|
||||
'code.copied': {
|
||||
id: 'code.copied',
|
||||
defaultMessage: 'Code copied',
|
||||
description: 'part of 15% discount code copied',
|
||||
},
|
||||
// authenticated user base component text
|
||||
'complete.your.profile.1': {
|
||||
id: 'complete.your.profile.1',
|
||||
defaultMessage: 'Complete',
|
||||
@@ -27,11 +16,6 @@ const messages = defineMessages({
|
||||
defaultMessage: '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;
|
||||
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,21 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form, Button,
|
||||
Button, Form,
|
||||
Icon,
|
||||
} from '@edx/paragon';
|
||||
import { Login } from '@edx/paragon/icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||
import messages from './messages';
|
||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||
|
||||
/**
|
||||
* This component renders the Single sign-on (SSO) button only for the tpa provider passed
|
||||
* */
|
||||
const EnterpriseSSO = (props) => {
|
||||
const { intl } = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const tpaProvider = props.provider;
|
||||
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
|
||||
|
||||
const handleSubmit = (e, url) => {
|
||||
e.preventDefault();
|
||||
@@ -33,7 +37,7 @@ const EnterpriseSSO = (props) => {
|
||||
<div className="d-flex flex-column">
|
||||
<div className="mw-450">
|
||||
<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
|
||||
id={tpaProvider.id}
|
||||
key={tpaProvider.id}
|
||||
@@ -44,16 +48,18 @@ const EnterpriseSSO = (props) => {
|
||||
>
|
||||
{tpaProvider.iconImage ? (
|
||||
<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>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<div className="font-container" aria-hidden="true">
|
||||
<FontAwesomeIcon
|
||||
icon={SUPPORTED_ICON_CLASSES.includes(tpaProvider.iconClass) ? ['fab', tpaProvider.iconClass] : faSignInAlt}
|
||||
/>
|
||||
<div className="btn-tpa__font-container" aria-hidden="true">
|
||||
{SUPPORTED_ICON_CLASSES.includes(tpaProvider.iconClass) ? (
|
||||
<FontAwesomeIcon icon={['fab', tpaProvider.iconClass]} />)
|
||||
: (
|
||||
<Icon className="h-75" src={Login} />
|
||||
)}
|
||||
</div>
|
||||
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
|
||||
</>
|
||||
@@ -62,12 +68,15 @@ const EnterpriseSSO = (props) => {
|
||||
<div className="mb-4" />
|
||||
<Button
|
||||
type="submit"
|
||||
id="other-ways-to-sign-in"
|
||||
variant="outline-primary"
|
||||
state="Complete"
|
||||
className="w-100"
|
||||
onClick={(e) => handleClick(e)}
|
||||
>
|
||||
{intl.formatMessage(messages['enterprisetpa.login.button.text'])}
|
||||
{disablePublicAccountCreation
|
||||
? formatMessage(messages['enterprisetpa.login.button.text.public.account.creation.disabled'])
|
||||
: formatMessage(messages['enterprisetpa.login.button.text'])}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
@@ -98,7 +107,6 @@ EnterpriseSSO.propTypes = {
|
||||
loginUrl: PropTypes.string,
|
||||
registerUrl: PropTypes.string,
|
||||
}),
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(EnterpriseSSO);
|
||||
export default EnterpriseSSO;
|
||||
|
||||
@@ -27,8 +27,9 @@ const FormGroup = (props) => {
|
||||
readOnly={props.readOnly}
|
||||
type={props.type}
|
||||
aria-invalid={props.errorMessage !== ''}
|
||||
className="form-field"
|
||||
className="form-group__form-field"
|
||||
autoComplete={props.autoComplete}
|
||||
spellCheck={props.spellCheck}
|
||||
name={props.name}
|
||||
value={props.value}
|
||||
onFocus={handleFocus}
|
||||
@@ -36,7 +37,6 @@ const FormGroup = (props) => {
|
||||
onClick={handleClick}
|
||||
onChange={props.handleChange}
|
||||
controlClassName={props.borderClass}
|
||||
|
||||
trailingElement={props.trailingElement}
|
||||
floatingLabel={props.floatingLabel}
|
||||
>
|
||||
@@ -64,41 +64,43 @@ const FormGroup = (props) => {
|
||||
|
||||
FormGroup.defaultProps = {
|
||||
as: 'input',
|
||||
errorMessage: '',
|
||||
borderClass: '',
|
||||
autoComplete: null,
|
||||
readOnly: false,
|
||||
handleBlur: null,
|
||||
handleChange: () => {},
|
||||
handleFocus: null,
|
||||
handleClick: null,
|
||||
helpText: [],
|
||||
options: null,
|
||||
trailingElement: null,
|
||||
type: 'text',
|
||||
borderClass: '',
|
||||
children: null,
|
||||
className: '',
|
||||
errorMessage: '',
|
||||
handleBlur: null,
|
||||
handleChange: () => {},
|
||||
handleClick: null,
|
||||
handleFocus: null,
|
||||
helpText: [],
|
||||
options: null,
|
||||
readOnly: false,
|
||||
spellCheck: null,
|
||||
trailingElement: null,
|
||||
type: 'text',
|
||||
};
|
||||
|
||||
FormGroup.propTypes = {
|
||||
as: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
borderClass: PropTypes.string,
|
||||
autoComplete: PropTypes.string,
|
||||
readOnly: PropTypes.bool,
|
||||
borderClass: PropTypes.string,
|
||||
children: PropTypes.element,
|
||||
className: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
floatingLabel: PropTypes.string.isRequired,
|
||||
handleBlur: PropTypes.func,
|
||||
handleChange: PropTypes.func,
|
||||
handleFocus: PropTypes.func,
|
||||
handleClick: PropTypes.func,
|
||||
handleFocus: PropTypes.func,
|
||||
helpText: PropTypes.arrayOf(PropTypes.string),
|
||||
name: PropTypes.string.isRequired,
|
||||
options: PropTypes.func,
|
||||
readOnly: PropTypes.bool,
|
||||
spellCheck: PropTypes.string,
|
||||
trailingElement: PropTypes.element,
|
||||
type: PropTypes.string,
|
||||
value: PropTypes.string.isRequired,
|
||||
children: PropTypes.element,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default FormGroup;
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import React from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Hyperlink, Icon } from '@edx/paragon';
|
||||
import { Institution } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* This component renders the Institution login button
|
||||
* */
|
||||
export const RenderInstitutionButton = props => {
|
||||
const { onSubmitHandler, buttonTitle } = props;
|
||||
|
||||
@@ -22,10 +27,13 @@ export const RenderInstitutionButton = props => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This component renders the page list of available institutions for login
|
||||
* */
|
||||
const InstitutionLogistration = props => {
|
||||
const lmsBaseUrl = getConfig().LMS_BASE_URL;
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
intl,
|
||||
secondaryProviders,
|
||||
headingTitle,
|
||||
} = props;
|
||||
@@ -34,11 +42,11 @@ const InstitutionLogistration = props => {
|
||||
<>
|
||||
<div className="d-flex justify-content-left mb-4 mt-2">
|
||||
<div className="flex-column">
|
||||
<h4 className="mb-2 font-weight-bold institute-heading">
|
||||
<h4 className="mb-2 font-weight-bold institutions__heading">
|
||||
{headingTitle}
|
||||
</h4>
|
||||
<p className="mb-2">
|
||||
{intl.formatMessage(messages['institution.login.page.sub.heading'])}
|
||||
{formatMessage(messages['institution.login.page.sub.heading'])}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,7 +57,7 @@ const InstitutionLogistration = props => {
|
||||
<tr key={provider} className="pgn__data-table-row">
|
||||
<td>
|
||||
<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}
|
||||
>
|
||||
{provider.name}
|
||||
@@ -87,7 +95,6 @@ RenderInstitutionButton.defaultProps = {
|
||||
|
||||
InstitutionLogistration.propTypes = {
|
||||
...LogistrationProps,
|
||||
intl: intlShape.isRequired,
|
||||
headingTitle: PropTypes.string,
|
||||
};
|
||||
InstitutionLogistration.defaultProps = {
|
||||
@@ -95,4 +102,4 @@ InstitutionLogistration.defaultProps = {
|
||||
headingTitle: '',
|
||||
};
|
||||
|
||||
export default injectIntl(InstitutionLogistration);
|
||||
export default InstitutionLogistration;
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthService } from '@edx/frontend-platform/auth';
|
||||
import {
|
||||
Tabs,
|
||||
Tab,
|
||||
Icon,
|
||||
} from '@edx/paragon';
|
||||
import { ChevronLeft } from '@edx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
||||
import { updatePathWithQueryParams, getTpaHint } from '../data/utils';
|
||||
import { LoginPage } from '../login';
|
||||
import { RegistrationPage } from '../register';
|
||||
import BaseComponent from '../base-component';
|
||||
|
||||
const Logistration = (props) => {
|
||||
const { intl, selectedPage } = props;
|
||||
const tpa = getTpaHint();
|
||||
const [institutionLogin, setInstitutionLogin] = useState(false);
|
||||
const [key, setKey] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const authService = getAuthService();
|
||||
if (authService) {
|
||||
authService.getCsrfTokenService().getCsrfToken(getConfig().LMS_BASE_URL);
|
||||
}
|
||||
});
|
||||
|
||||
const handleInstitutionLogin = (e) => {
|
||||
sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
|
||||
if (typeof e === 'string') {
|
||||
sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register');
|
||||
} else {
|
||||
sendPageEvent('login_and_registration', e.target.dataset.eventName);
|
||||
}
|
||||
|
||||
setInstitutionLogin(!institutionLogin);
|
||||
};
|
||||
|
||||
const handleOnSelect = (tabKey) => {
|
||||
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
|
||||
setKey(tabKey);
|
||||
};
|
||||
|
||||
const tabTitle = (
|
||||
<div className="d-flex">
|
||||
<Icon src={ChevronLeft} className="left-icon" />
|
||||
<span className="ml-2">
|
||||
{selectedPage === LOGIN_PAGE
|
||||
? intl.formatMessage(messages['logistration.sign.in'])
|
||||
: intl.formatMessage(messages['logistration.register'])}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseComponent isRegistrationPage={selectedPage === REGISTER_PAGE}>
|
||||
<div>
|
||||
{institutionLogin
|
||||
? (
|
||||
<Tabs defaultActiveKey="" id="controlled-tab" onSelect={handleInstitutionLogin}>
|
||||
<Tab title={tabTitle} eventKey={selectedPage === LOGIN_PAGE ? LOGIN_PAGE : REGISTER_PAGE} />
|
||||
</Tabs>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
{!tpa && (
|
||||
<Tabs defaultActiveKey={selectedPage} id="controlled-tab" onSelect={handleOnSelect}>
|
||||
<Tab title={intl.formatMessage(messages['logistration.register'])} eventKey={REGISTER_PAGE} />
|
||||
<Tab title={intl.formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_PAGE} />
|
||||
</Tabs>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{ key && (
|
||||
<Redirect to={updatePathWithQueryParams(key)} />
|
||||
)}
|
||||
<div id="main-content" className="main-content">
|
||||
{selectedPage === LOGIN_PAGE
|
||||
? <LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
|
||||
: <RegistrationPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />}
|
||||
</div>
|
||||
</div>
|
||||
</BaseComponent>
|
||||
);
|
||||
};
|
||||
|
||||
Logistration.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
selectedPage: PropTypes.string,
|
||||
};
|
||||
|
||||
Logistration.defaultProps = {
|
||||
selectedPage: REGISTER_PAGE,
|
||||
};
|
||||
|
||||
export default injectIntl(Logistration);
|
||||
@@ -1,16 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<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">
|
||||
<FormattedMessage
|
||||
id="error.notfound.message"
|
||||
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
|
||||
description="error message when a page does not exist"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const NotFoundPage = () => (
|
||||
<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">
|
||||
<FormattedMessage
|
||||
id="error.notfound.message"
|
||||
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
|
||||
description="error message when a page does not exist"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default NotFoundPage;
|
||||
|
||||
@@ -1,53 +1,115 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form, IconButton, useToggle, Tooltip, OverlayTrigger, Icon,
|
||||
Form, Icon, IconButton, OverlayTrigger, Tooltip, useToggle,
|
||||
} from '@edx/paragon';
|
||||
import {
|
||||
Check, Remove, Visibility, VisibilityOff,
|
||||
} from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
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 { formatMessage } = props.intl;
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const validationApiRateLimited = useSelector(state => state.register.validationApiRateLimited);
|
||||
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
const handleBlur = (e) => {
|
||||
if (props.handleBlur) { props.handleBlur(e); }
|
||||
const { name, value } = e.target;
|
||||
if (name === props.name && e.relatedTarget?.name === 'passwordIcon') {
|
||||
return; // Do not run validations on password icon click
|
||||
}
|
||||
|
||||
let passwordValue = value;
|
||||
if (name === 'passwordIcon') {
|
||||
// To validate actual password value when onBlur is triggered by focusing out the password icon
|
||||
passwordValue = props.value;
|
||||
}
|
||||
|
||||
if (props.handleBlur) {
|
||||
props.handleBlur({
|
||||
target: {
|
||||
name: props.name,
|
||||
value: passwordValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setShowTooltip(props.showRequirements && false);
|
||||
if (props.handleErrorChange) { // If rendering from register page
|
||||
const fieldError = validatePasswordField(passwordValue, formatMessage);
|
||||
if (fieldError) {
|
||||
props.handleErrorChange('password', fieldError);
|
||||
} else if (!validationApiRateLimited) {
|
||||
dispatch(fetchRealtimeValidations({ password: passwordValue }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocus = (e) => {
|
||||
if (e.target?.name === 'passwordIcon') {
|
||||
return; // Do not clear error on password icon focus
|
||||
}
|
||||
|
||||
if (props.handleFocus) {
|
||||
props.handleFocus(e);
|
||||
}
|
||||
if (props.handleErrorChange) {
|
||||
props.handleErrorChange('password', '');
|
||||
dispatch(clearRegistrationBackendError('password'));
|
||||
}
|
||||
setTimeout(() => setShowTooltip(props.showRequirements && true), 150);
|
||||
};
|
||||
|
||||
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 = (
|
||||
<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 tooltip = (
|
||||
<Tooltip id={`password-requirement-${placement}`}>
|
||||
<span id="letter-check" className="d-flex position-relative align-content-start">
|
||||
<span id="letter-check" className="d-flex align-items-center">
|
||||
{LETTER_REGEX.test(props.value) ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1 text-light-700" src={Remove} />}
|
||||
{formatMessage(messages['one.letter'])}
|
||||
</span>
|
||||
<span id="number-check" className="d-flex position-relative align-content-start">
|
||||
<span id="number-check" className="d-flex align-items-center">
|
||||
{NUMBER_REGEX.test(props.value) ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1 text-light-700" src={Remove} />}
|
||||
{formatMessage(messages['one.number'])}
|
||||
</span>
|
||||
<span id="characters-check" className="d-flex position-relative align-content-start">
|
||||
<span id="characters-check" className="d-flex align-items-center">
|
||||
{props.value.length >= 8 ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1 text-light-700" src={Remove} />}
|
||||
{formatMessage(messages['eight.characters'])}
|
||||
</span>
|
||||
@@ -59,10 +121,11 @@ const PasswordField = (props) => {
|
||||
<OverlayTrigger key="tooltip" placement={placement} overlay={tooltip} show={showTooltip}>
|
||||
<Form.Control
|
||||
as="input"
|
||||
className="form-field"
|
||||
className="form-group__form-field"
|
||||
type={isPasswordHidden ? 'password' : 'text'}
|
||||
name={props.name}
|
||||
value={props.value}
|
||||
autoComplete={props.autoComplete}
|
||||
aria-invalid={props.errorMessage !== ''}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
@@ -88,7 +151,9 @@ PasswordField.defaultProps = {
|
||||
handleBlur: null,
|
||||
handleFocus: null,
|
||||
handleChange: () => {},
|
||||
handleErrorChange: null,
|
||||
showRequirements: true,
|
||||
autoComplete: null,
|
||||
};
|
||||
|
||||
PasswordField.propTypes = {
|
||||
@@ -98,10 +163,11 @@ PasswordField.propTypes = {
|
||||
handleBlur: PropTypes.func,
|
||||
handleFocus: PropTypes.func,
|
||||
handleChange: PropTypes.func,
|
||||
intl: intlShape.isRequired,
|
||||
handleErrorChange: PropTypes.func,
|
||||
name: PropTypes.string.isRequired,
|
||||
showRequirements: PropTypes.bool,
|
||||
value: PropTypes.string.isRequired,
|
||||
autoComplete: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(PasswordField);
|
||||
export default PasswordField;
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import { WELCOME_PAGE } from '../data/constants';
|
||||
import {
|
||||
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
|
||||
} from '../data/constants';
|
||||
import { setCookie } from '../data/utils';
|
||||
|
||||
function RedirectLogistration(props) {
|
||||
const RedirectLogistration = (props) => {
|
||||
const {
|
||||
finishAuthUrl, redirectUrl, redirectToWelcomePage, success,
|
||||
authenticatedUser,
|
||||
finishAuthUrl,
|
||||
redirectUrl,
|
||||
redirectToProgressiveProfilingPage,
|
||||
success,
|
||||
optionalFields,
|
||||
redirectToRecommendationsPage,
|
||||
educationLevel,
|
||||
userId,
|
||||
registrationEmbedded,
|
||||
host,
|
||||
} = props;
|
||||
let finalRedirectUrl = '';
|
||||
|
||||
@@ -25,31 +34,80 @@ function RedirectLogistration(props) {
|
||||
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);
|
||||
// use this component to redirect WelcomePage after successful registration
|
||||
// return <Redirect to={WELCOME_PAGE} />;
|
||||
|
||||
if (registrationEmbedded) {
|
||||
window.parent.postMessage({
|
||||
action: REDIRECT,
|
||||
redirectUrl: getConfig().POST_REGISTRATION_REDIRECT_URL,
|
||||
}, host);
|
||||
return null;
|
||||
}
|
||||
const registrationResult = { redirectUrl: finalRedirectUrl, success };
|
||||
return <Redirect to={{ pathname: WELCOME_PAGE, state: { registrationResult } }} />;
|
||||
return (
|
||||
<Navigate
|
||||
to={AUTHN_PROGRESSIVE_PROFILING}
|
||||
state={{
|
||||
registrationResult,
|
||||
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;
|
||||
}
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
RedirectLogistration.defaultProps = {
|
||||
authenticatedUser: {},
|
||||
educationLevel: null,
|
||||
finishAuthUrl: null,
|
||||
success: false,
|
||||
redirectUrl: '',
|
||||
redirectToWelcomePage: false,
|
||||
redirectToProgressiveProfilingPage: false,
|
||||
optionalFields: {},
|
||||
redirectToRecommendationsPage: false,
|
||||
userId: null,
|
||||
registrationEmbedded: false,
|
||||
host: '',
|
||||
};
|
||||
|
||||
RedirectLogistration.propTypes = {
|
||||
authenticatedUser: PropTypes.shape({}),
|
||||
educationLevel: PropTypes.string,
|
||||
finishAuthUrl: PropTypes.string,
|
||||
success: PropTypes.bool,
|
||||
redirectUrl: PropTypes.string,
|
||||
redirectToWelcomePage: PropTypes.bool,
|
||||
redirectToProgressiveProfilingPage: PropTypes.bool,
|
||||
optionalFields: PropTypes.shape({}),
|
||||
redirectToRecommendationsPage: PropTypes.bool,
|
||||
userId: PropTypes.number,
|
||||
registrationEmbedded: PropTypes.bool,
|
||||
host: PropTypes.string,
|
||||
};
|
||||
|
||||
export default RedirectLogistration;
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@edx/paragon';
|
||||
import { Login } from '@edx/paragon/icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||
|
||||
function SocialAuthProviders(props) {
|
||||
const { intl, referrer, socialAuthProviders } = props;
|
||||
const SocialAuthProviders = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { referrer, socialAuthProviders } = props;
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
@@ -30,29 +32,30 @@ function SocialAuthProviders(props) {
|
||||
>
|
||||
{provider.iconImage ? (
|
||||
<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 className="font-container" aria-hidden="true">
|
||||
<FontAwesomeIcon
|
||||
icon={SUPPORTED_ICON_CLASSES.includes(provider.iconClass) ? ['fab', provider.iconClass] : faSignInAlt}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<div className="btn-tpa__font-container" aria-hidden="true">
|
||||
{SUPPORTED_ICON_CLASSES.includes(provider.iconClass) ? (
|
||||
<FontAwesomeIcon icon={['fab', provider.iconClass]} />)
|
||||
: (
|
||||
<Icon className="h-75" src={Login} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<span id="provider-name" className="notranslate mr-auto pl-2" aria-hidden="true">{provider.name}</span>
|
||||
<span className="sr-only">
|
||||
{referrer === LOGIN_PAGE
|
||||
? intl.formatMessage(messages['sso.sign.in.with'], { providerName: provider.name })
|
||||
: intl.formatMessage(messages['sso.create.account.using'], { providerName: provider.name })}
|
||||
? formatMessage(messages['sso.sign.in.with'], { providerName: provider.name })
|
||||
: formatMessage(messages['sso.create.account.using'], { providerName: provider.name })}
|
||||
</span>
|
||||
</button>
|
||||
));
|
||||
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{socialAuth}</>;
|
||||
}
|
||||
};
|
||||
|
||||
SocialAuthProviders.defaultProps = {
|
||||
referrer: LOGIN_PAGE,
|
||||
@@ -60,7 +63,6 @@ SocialAuthProviders.defaultProps = {
|
||||
};
|
||||
|
||||
SocialAuthProviders.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
referrer: PropTypes.string,
|
||||
socialAuthProviders: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
@@ -69,7 +71,8 @@ SocialAuthProviders.propTypes = {
|
||||
iconImage: PropTypes.string,
|
||||
loginUrl: PropTypes.string,
|
||||
registerUrl: PropTypes.string,
|
||||
skipRegistrationForm: PropTypes.bool,
|
||||
})),
|
||||
};
|
||||
|
||||
export default injectIntl(SocialAuthProviders);
|
||||
export default SocialAuthProviders;
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TransitionReplace } from '@edx/paragon';
|
||||
|
||||
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 PropTypes from 'prop-types';
|
||||
|
||||
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 PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
||||
|
||||
const ThirdPartyAuthAlert = (props) => {
|
||||
const { currentProvider, intl, referrer } = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const { currentProvider, referrer } = props;
|
||||
const platformName = getConfig().SITE_NAME;
|
||||
let message;
|
||||
|
||||
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 {
|
||||
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 (
|
||||
<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 ? (
|
||||
<Alert.Heading>{formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
|
||||
) : null}
|
||||
<p>{ message }</p>
|
||||
</Alert>
|
||||
{referrer === REGISTER_PAGE ? (
|
||||
<Alert.Heading>{intl.formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
|
||||
<h4 className="mt-4 mb-4">{formatMessage(messages['registration.using.tpa.form.heading'])}</h4>
|
||||
) : null}
|
||||
<p>{ message }</p>
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ThirdPartyAuthAlert.defaultProps = {
|
||||
currentProvider: '',
|
||||
referrer: LOGIN_PAGE,
|
||||
};
|
||||
|
||||
ThirdPartyAuthAlert.propTypes = {
|
||||
currentProvider: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
currentProvider: PropTypes.string,
|
||||
referrer: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(ThirdPartyAuthAlert);
|
||||
export default ThirdPartyAuthAlert;
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { DEFAULT_REDIRECT_URL } from '../data/constants';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
DEFAULT_REDIRECT_URL,
|
||||
} from '../data/constants';
|
||||
|
||||
/**
|
||||
* This wrapper redirects the requester to our default redirect url if they are
|
||||
* already authenticated.
|
||||
*/
|
||||
const UnAuthOnlyRoute = (props) => {
|
||||
const UnAuthOnlyRoute = ({ children }) => {
|
||||
const [authUser, setAuthUser] = useState({});
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
|
||||
@@ -25,10 +29,14 @@ const UnAuthOnlyRoute = (props) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Route {...props} />;
|
||||
return children;
|
||||
}
|
||||
|
||||
return <></>;
|
||||
return null;
|
||||
};
|
||||
|
||||
UnAuthOnlyRoute.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
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';
|
||||
|
||||
export const THIRD_PARTY_AUTH_CONTEXT = new AsyncActionType('THIRD_PARTY_AUTH', 'GET_THIRD_PARTY_AUTH_CONTEXT');
|
||||
export const THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG = 'THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG';
|
||||
|
||||
// Third party auth context
|
||||
export const getThirdPartyAuthContext = (urlParams) => ({
|
||||
@@ -12,11 +13,15 @@ export const getThirdPartyAuthContextBegin = () => ({
|
||||
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
|
||||
});
|
||||
|
||||
export const getThirdPartyAuthContextSuccess = (fieldDescriptions, thirdPartyAuthContext) => ({
|
||||
export const getThirdPartyAuthContextSuccess = (fieldDescriptions, optionalFields, thirdPartyAuthContext) => ({
|
||||
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
|
||||
payload: { fieldDescriptions, thirdPartyAuthContext },
|
||||
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
|
||||
});
|
||||
|
||||
export const getThirdPartyAuthContextFailure = () => ({
|
||||
type: THIRD_PARTY_AUTH_CONTEXT.FAILURE,
|
||||
});
|
||||
|
||||
export const clearThirdPartyAuthContextErrorMessage = () => ({
|
||||
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
|
||||
});
|
||||
|
||||
@@ -1,32 +1,61 @@
|
||||
import { THIRD_PARTY_AUTH_CONTEXT } from './actions';
|
||||
|
||||
import { PENDING_STATE, COMPLETE_STATE } from '../../data/constants';
|
||||
import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from './actions';
|
||||
import { COMPLETE_STATE, FAILURE_STATE, PENDING_STATE } from '../../data/constants';
|
||||
|
||||
export const defaultState = {
|
||||
extendedProfile: [],
|
||||
fieldDescriptions: {},
|
||||
fieldDescriptions: {
|
||||
fields: {},
|
||||
},
|
||||
optionalFields: {
|
||||
fields: {},
|
||||
extended_profile: [],
|
||||
},
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext: {
|
||||
autoSubmitRegForm: false,
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
countryCode: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
pipelineUserDetails: null,
|
||||
errorMessage: null,
|
||||
welcomePageRedirectUrl: null,
|
||||
},
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action) => {
|
||||
const reducer = (state = defaultState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case THIRD_PARTY_AUTH_CONTEXT.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||
};
|
||||
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS:
|
||||
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS: {
|
||||
return {
|
||||
...state,
|
||||
extendedProfile: action.payload.fieldDescriptions.extendedProfile,
|
||||
fieldDescriptions: action.payload.fieldDescriptions.fields,
|
||||
optionalFields: action.payload.optionalFields,
|
||||
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
};
|
||||
}
|
||||
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
thirdPartyAuthApiStatus: 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:
|
||||
return state;
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
// Actions
|
||||
import {
|
||||
THIRD_PARTY_AUTH_CONTEXT,
|
||||
getThirdPartyAuthContextBegin,
|
||||
getThirdPartyAuthContextSuccess,
|
||||
getThirdPartyAuthContextFailure,
|
||||
getThirdPartyAuthContextSuccess,
|
||||
THIRD_PARTY_AUTH_CONTEXT,
|
||||
} from './actions';
|
||||
|
||||
// Services
|
||||
import {
|
||||
getThirdPartyAuthContext,
|
||||
} from './service';
|
||||
import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions';
|
||||
|
||||
export function* fetchThirdPartyAuthContext(action) {
|
||||
try {
|
||||
yield put(getThirdPartyAuthContextBegin());
|
||||
const { fieldDescriptions, thirdPartyAuthContext } = yield call(getThirdPartyAuthContext, action.payload.urlParams);
|
||||
const {
|
||||
fieldDescriptions, optionalFields, thirdPartyAuthContext,
|
||||
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
|
||||
|
||||
yield put(getThirdPartyAuthContextSuccess(
|
||||
fieldDescriptions, thirdPartyAuthContext,
|
||||
));
|
||||
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
|
||||
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
|
||||
} catch (e) {
|
||||
yield put(getThirdPartyAuthContextFailure());
|
||||
logError(e);
|
||||
|
||||
@@ -14,7 +14,15 @@ export const fieldDescriptionSelector = createSelector(
|
||||
commonComponents => commonComponents.fieldDescriptions,
|
||||
);
|
||||
|
||||
export const extendedProfileSelector = createSelector(
|
||||
export const optionalFieldsSelector = createSelector(
|
||||
commonComponentsSelector,
|
||||
commonComponents => commonComponents.extendedProfile,
|
||||
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';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
@@ -18,11 +18,8 @@ export async function getThirdPartyAuthContext(urlParams) {
|
||||
throw (e);
|
||||
});
|
||||
return {
|
||||
fieldDescriptions: data.registration_fields || {},
|
||||
// For backward compatibility with the API, once https://github.com/openedx/edx-platform/pull/30198 is merged
|
||||
// and deployed update it to use data.context_data
|
||||
thirdPartyAuthContext: camelCaseObject(
|
||||
convertKeyNames(data.context_data || data, { fullname: 'name' }),
|
||||
),
|
||||
fieldDescriptions: data.registrationFields || {},
|
||||
optionalFields: data.optionalFields || {},
|
||||
thirdPartyAuthContext: data.contextData || {},
|
||||
};
|
||||
}
|
||||
|
||||
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,9 +1,10 @@
|
||||
import { runSaga } from 'redux-saga';
|
||||
|
||||
import { setCountryFromThirdPartyAuthContext } from '../../../register/data/actions';
|
||||
import initializeMockLogging from '../../../setupTest';
|
||||
import * as actions from '../actions';
|
||||
import { fetchThirdPartyAuthContext } from '../sagas';
|
||||
import * as api from '../service';
|
||||
import initializeMockLogging from '../../../setupTest';
|
||||
|
||||
const { loggingService } = initializeMockLogging();
|
||||
|
||||
@@ -26,7 +27,11 @@ describe('fetchThirdPartyAuthContext', () => {
|
||||
|
||||
it('should call service and dispatch success action', async () => {
|
||||
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
|
||||
.mockImplementation(() => Promise.resolve({ thirdPartyAuthContext: data, fieldDescriptions: {} }));
|
||||
.mockImplementation(() => Promise.resolve({
|
||||
thirdPartyAuthContext: data,
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {},
|
||||
}));
|
||||
|
||||
const dispatched = [];
|
||||
await runSaga(
|
||||
@@ -38,7 +43,8 @@ describe('fetchThirdPartyAuthContext', () => {
|
||||
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
|
||||
expect(dispatched).toEqual([
|
||||
actions.getThirdPartyAuthContextBegin(),
|
||||
actions.getThirdPartyAuthContextSuccess({}, data),
|
||||
setCountryFromThirdPartyAuthContext(),
|
||||
actions.getThirdPartyAuthContextSuccess({}, {}, data),
|
||||
]);
|
||||
getThirdPartyAuthContext.mockClear();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { default as RedirectLogistration } from './RedirectLogistration';
|
||||
export { default as registerIcons } from './RegisterFaIcons';
|
||||
export { default as EmbeddedRegistrationRoute } from './EmbeddedRegistrationRoute';
|
||||
export { default as UnAuthOnlyRoute } from './UnAuthOnlyRoute';
|
||||
export { default as NotFoundPage } from './NotFoundPage';
|
||||
export { default as SocialAuthProviders } from './SocialAuthProviders';
|
||||
@@ -11,4 +12,4 @@ export { default as saga } from './data/sagas';
|
||||
export { storeName } from './data/selectors';
|
||||
export { default as FormGroup } from './FormGroup';
|
||||
export { default as PasswordField } from './PasswordField';
|
||||
export { default as Logistration } from './Logistration';
|
||||
export { default as Zendesk } from './Zendesk';
|
||||
|
||||
@@ -1,29 +1,13 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
// institution login strings
|
||||
'institution.login.page.sub.heading': {
|
||||
id: 'institution.login.page.sub.heading',
|
||||
defaultMessage: 'Choose your institution from the list below',
|
||||
description: 'Heading of the institutions list',
|
||||
},
|
||||
// Confirmation Alert Message
|
||||
'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 strings
|
||||
'logistration.sign.in': {
|
||||
id: 'logistration.sign.in',
|
||||
defaultMessage: 'Sign in',
|
||||
@@ -34,32 +18,22 @@ const messages = defineMessages({
|
||||
defaultMessage: '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
|
||||
'enterprisetpa.title.heading': {
|
||||
id: 'enterprisetpa.title.heading',
|
||||
defaultMessage: 'Would you like to sign in using your {providerName} credentials?',
|
||||
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': {
|
||||
id: 'enterprisetpa.login.button.text',
|
||||
defaultMessage: 'Show me other ways to sign in or register',
|
||||
description: 'Button text for login',
|
||||
},
|
||||
'enterprisetpa.login.button.text.public.account.creation.disabled': {
|
||||
id: 'enterprisetpa.login.button.text.public.account.creation.disabled',
|
||||
defaultMessage: 'Show me other ways to sign in',
|
||||
description: 'Button text for login when account creation is disabled',
|
||||
},
|
||||
// social auth providers
|
||||
'sso.sign.in.with': {
|
||||
id: 'sso.sign.in.with',
|
||||
@@ -84,17 +58,17 @@ const messages = defineMessages({
|
||||
},
|
||||
'one.letter': {
|
||||
id: 'one.letter',
|
||||
defaultMessage: '1 Letter',
|
||||
defaultMessage: '1 letter',
|
||||
description: 'password requirement to have 1 letter',
|
||||
},
|
||||
'one.number': {
|
||||
id: 'one.number',
|
||||
defaultMessage: '1 Number',
|
||||
defaultMessage: '1 number',
|
||||
description: 'password requirement to have 1 number',
|
||||
},
|
||||
'eight.characters': {
|
||||
id: 'eight.characters',
|
||||
defaultMessage: '8 Characters',
|
||||
defaultMessage: '8 characters',
|
||||
description: 'password requirement to have a minimum of 8 characters',
|
||||
},
|
||||
'password.sr.only.helping.text': {
|
||||
@@ -123,6 +97,21 @@ const messages = defineMessages({
|
||||
description: 'Message that appears on register page if user has successfully authenticated with TPA '
|
||||
+ '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;
|
||||
|
||||
@@ -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 { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import { fetchRealtimeValidations } from '../../register/data/actions';
|
||||
import FormGroup from '../FormGroup';
|
||||
import PasswordField from '../PasswordField';
|
||||
|
||||
@@ -26,10 +30,27 @@ describe('FormGroup', () => {
|
||||
});
|
||||
|
||||
describe('PasswordField', () => {
|
||||
const mockStore = configureStore();
|
||||
const IntlPasswordField = injectIntl(PasswordField);
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</MemoryRouter>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const initialState = {
|
||||
register: {
|
||||
validationApiRateLimited: false,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(initialState);
|
||||
props = {
|
||||
floatingLabel: 'Password',
|
||||
name: 'password',
|
||||
@@ -39,7 +60,7 @@ describe('PasswordField', () => {
|
||||
});
|
||||
|
||||
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');
|
||||
expect(passwordField.find('input').prop('type')).toEqual('text');
|
||||
@@ -49,7 +70,7 @@ describe('PasswordField', () => {
|
||||
});
|
||||
|
||||
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();
|
||||
await act(async () => {
|
||||
passwordField.find('input').simulate('focus');
|
||||
@@ -67,7 +88,7 @@ describe('PasswordField', () => {
|
||||
};
|
||||
|
||||
jest.useFakeTimers();
|
||||
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
await act(async () => {
|
||||
passwordField.find('input').simulate('focus');
|
||||
jest.runAllTimers();
|
||||
@@ -80,7 +101,7 @@ describe('PasswordField', () => {
|
||||
});
|
||||
|
||||
it('should update password requirement checks', async () => {
|
||||
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
jest.useFakeTimers();
|
||||
await act(async () => {
|
||||
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('#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,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import SocialAuthProviders from '../SocialAuthProviders';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import registerIcons from '../RegisterFaIcons';
|
||||
import SocialAuthProviders from '../SocialAuthProviders';
|
||||
|
||||
registerIcons();
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import ThirdPartyAuthAlert from '../ThirdPartyAuthAlert';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import { REGISTER_PAGE } from '../../data/constants';
|
||||
import ThirdPartyAuthAlert from '../ThirdPartyAuthAlert';
|
||||
|
||||
describe('ThirdPartyAuthAlert', () => {
|
||||
let props = {};
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
/* eslint-disable import/no-import-module-exports */
|
||||
/* eslint-disable react/function-component-definition */
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { BrowserRouter as Router, MemoryRouter, Switch } from 'react-router-dom';
|
||||
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
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');
|
||||
// Just render plain div with its children
|
||||
@@ -18,16 +27,16 @@ module.exports = RRD;
|
||||
const TestApp = () => (
|
||||
<Router>
|
||||
<div>
|
||||
<Switch>
|
||||
<UnAuthOnlyRoute path={LOGIN_PAGE} render={() => (<span>Login Page</span>)} />
|
||||
</Switch>
|
||||
<Routes>
|
||||
<Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><span>Register Page</span></UnAuthOnlyRoute>} />
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
|
||||
describe('UnAuthOnlyRoute', () => {
|
||||
const routerWrapper = () => (
|
||||
<MemoryRouter initialEntries={[LOGIN_PAGE]}>
|
||||
<MemoryRouter initialEntries={[REGISTER_PAGE]}>
|
||||
<TestApp />
|
||||
</MemoryRouter>
|
||||
);
|
||||
@@ -36,25 +45,30 @@ describe('UnAuthOnlyRoute', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should have called with forceRefresh true', () => {
|
||||
it('should have called with forceRefresh true', async () => {
|
||||
const user = {
|
||||
username: 'gonzo',
|
||||
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());
|
||||
});
|
||||
|
||||
expect(fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: true });
|
||||
});
|
||||
|
||||
it('should have called with forceRefresh false', () => {
|
||||
auth.getAuthenticatedUser = jest.fn(() => null);
|
||||
auth.fetchAuthenticatedUser = jest.fn(() => ({ then: () => auth.getAuthenticatedUser() }));
|
||||
it('should have called with forceRefresh false', async () => {
|
||||
getAuthenticatedUser.mockReturnValue(null);
|
||||
fetchAuthenticatedUser.mockReturnValueOnce(Promise.resolve(null));
|
||||
|
||||
mount(routerWrapper());
|
||||
await act(async () => {
|
||||
await mount(routerWrapper());
|
||||
});
|
||||
|
||||
expect(auth.fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: false });
|
||||
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
|
||||
aria-hidden="true"
|
||||
className="font-container"
|
||||
className="btn-tpa__font-container"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-sign-in-alt fa-w-16 "
|
||||
data-icon="sign-in-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<span
|
||||
className="pgn__icon h-75"
|
||||
>
|
||||
<path
|
||||
d="M416 448h-84c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h84c17.7 0 32-14.3 32-32V160c0-17.7-14.3-32-32-32h-84c-6.6 0-12-5.4-12-12V76c0-6.6 5.4-12 12-12h84c53 0 96 43 96 96v192c0 53-43 96-96 96zm-47-201L201 79c-15-15-41-4.5-41 17v96H24c-13.3 0-24 10.7-24 24v96c0 13.3 10.7 24 24 24h136v96c0 21.5 26 32 41 17l168-168c9.3-9.4 9.3-24.6 0-34z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11 7 9.6 8.4l2.6 2.6H2v2h10.2l-2.6 2.6L11 17l5-5-5-5zm9 12h-8v2h10V3H12v2h8v14z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -55,11 +57,11 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="font-container"
|
||||
className="btn-tpa__font-container"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-google fa-w-16 "
|
||||
className="svg-inline--fa fa-google "
|
||||
data-icon="google"
|
||||
data-prefix="fab"
|
||||
focusable="false"
|
||||
@@ -104,7 +106,7 @@ Array [
|
||||
>
|
||||
<img
|
||||
alt="icon Apple"
|
||||
className="icon-image"
|
||||
className="btn-tpa__image-icon"
|
||||
src="https://edx.devstack.lms/logo.png"
|
||||
/>
|
||||
</div>
|
||||
@@ -133,7 +135,7 @@ Array [
|
||||
>
|
||||
<img
|
||||
alt="icon Facebook"
|
||||
className="icon-image"
|
||||
className="btn-tpa__image-icon"
|
||||
src="https://edx.devstack.lms/facebook-logo.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`ThirdPartyAuthAlert should match login page third party auth alert message snapshot 1`] = `
|
||||
<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"
|
||||
role="alert"
|
||||
>
|
||||
@@ -21,26 +21,33 @@ exports[`ThirdPartyAuthAlert should match login page third party auth alert mess
|
||||
`;
|
||||
|
||||
exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = `
|
||||
<div
|
||||
className="fade alert-content alert-success mt-n2 alert show"
|
||||
id="tpa-alert"
|
||||
role="alert"
|
||||
>
|
||||
Array [
|
||||
<div
|
||||
className="pgn__alert-message-wrapper"
|
||||
className="fade alert-content alert-success mt-n2 mb-5 alert show"
|
||||
id="tpa-alert"
|
||||
role="alert"
|
||||
>
|
||||
<div
|
||||
className="alert-message-content"
|
||||
className="pgn__alert-message-wrapper"
|
||||
>
|
||||
<div
|
||||
className="alert-heading h4"
|
||||
className="alert-message-content"
|
||||
>
|
||||
Almost done!
|
||||
<div
|
||||
className="alert-heading h4"
|
||||
>
|
||||
Almost done!
|
||||
</div>
|
||||
<p>
|
||||
You've successfully signed into Google! We just need a little more information before you start learning with Your Platform Name Here.
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
You've successfully signed into Google! We just need a little more information before you start learning with Your Platform Name Here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</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,
|
||||
USER_RETENTION_COOKIE_NAME: process.env.USER_RETENTION_COOKIE_NAME || '',
|
||||
// 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,9 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { applyMiddleware, createStore, compose } from 'redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import { composeWithDevTools } from '@redux-devtools/extension';
|
||||
import { applyMiddleware, compose, createStore } from 'redux';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import createSagaMiddleware from 'redux-saga';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
|
||||
import createRootReducer from './reducers';
|
||||
import rootSaga from './sagas';
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// URL Paths
|
||||
export const LOGIN_PAGE = '/login';
|
||||
export const REGISTER_PAGE = '/register';
|
||||
export const REGISTER_EMBEDDED_PAGE = '/register-embedded';
|
||||
export const RESET_PAGE = '/reset';
|
||||
export const WELCOME_PAGE = '/welcome';
|
||||
export const AUTHN_PROGRESSIVE_PROFILING = '/welcome';
|
||||
export const DEFAULT_REDIRECT_URL = '/dashboard';
|
||||
export const RECOMMENDATIONS = '/recommendations';
|
||||
export const PASSWORD_RESET_CONFIRM = '/password_reset_confirm/:token/';
|
||||
export const PAGE_NOT_FOUND = '/notfound';
|
||||
export const ENTERPRISE_LOGIN_URL = '/enterprise/login';
|
||||
@@ -12,24 +14,26 @@ export const ENTERPRISE_LOGIN_URL = '/enterprise/login';
|
||||
export const SUPPORTED_ICON_CLASSES = ['apple', 'facebook', 'google', 'microsoft'];
|
||||
|
||||
// Error Codes
|
||||
export const FORM_SUBMISSION_ERROR = 'form-submission-error';
|
||||
export const INTERNAL_SERVER_ERROR = 'internal-server-error';
|
||||
export const API_RATELIMIT_ERROR = 'api-ratelimit-error';
|
||||
|
||||
// States
|
||||
// Common States
|
||||
export const DEFAULT_STATE = 'default';
|
||||
export const PENDING_STATE = 'pending';
|
||||
export const COMPLETE_STATE = 'complete';
|
||||
export const FAILURE_STATE = 'failure';
|
||||
export const FORBIDDEN_STATE = 'forbidden';
|
||||
export const EMBEDDED = 'embedded';
|
||||
|
||||
// Regex
|
||||
export const LETTER_REGEX = /[a-zA-Z]/;
|
||||
export const NUMBER_REGEX = /\d/;
|
||||
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'
|
||||
+ '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"'
|
||||
+ ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63})'
|
||||
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$';
|
||||
export const LETTER_REGEX = /[a-zA-Z]/;
|
||||
export const NUMBER_REGEX = /\d/;
|
||||
export const 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
|
||||
// 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();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user