Compare commits
347 Commits
fixing-san
...
sajjad/aut
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db75d5046d | ||
|
|
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 | ||
|
|
ad8be560b4 | ||
|
|
5dea66540d | ||
|
|
400648f09f | ||
|
|
f781f311a2 | ||
|
|
9469adc386 | ||
|
|
691a248477 | ||
|
|
4bf0021982 | ||
|
|
b04257a62d | ||
|
|
bce8aa712d | ||
|
|
25bc11ea8d | ||
|
|
bcece267bc | ||
|
|
1575034ed3 | ||
|
|
922f1e2ac4 | ||
|
|
b03fe545ba | ||
|
|
47fce8aa7d | ||
|
|
5fd6754025 | ||
|
|
428372d198 | ||
|
|
4f83726387 | ||
|
|
40cb949d4e | ||
|
|
daa54be1c8 | ||
|
|
b126635981 | ||
|
|
94e1207d37 | ||
|
|
9ec6e1cf05 | ||
|
|
eddce35a37 | ||
|
|
38cbd4d0d8 | ||
|
|
b962b3f4ca | ||
|
|
ad9b40d9e9 | ||
|
|
7990fe7483 | ||
|
|
def1415f14 |
27
.env
27
.env
@@ -13,16 +13,23 @@ ORDER_HISTORY_URL=null
|
|||||||
REFRESH_ACCESS_TOKEN_ENDPOINT=null
|
REFRESH_ACCESS_TOKEN_ENDPOINT=null
|
||||||
SEGMENT_KEY=''
|
SEGMENT_KEY=''
|
||||||
SITE_NAME=null
|
SITE_NAME=null
|
||||||
USER_INFO_COOKIE_NAME=null
|
|
||||||
AUTHN_MINIMAL_HEADER=true
|
|
||||||
LOGIN_ISSUE_SUPPORT_LINK=''
|
|
||||||
USER_SURVEY_COOKIE_NAME=null
|
|
||||||
COOKIE_DOMAIN=null
|
|
||||||
WELCOME_PAGE_SUPPORT_LINK=null
|
|
||||||
INFO_EMAIL=''
|
INFO_EMAIL=''
|
||||||
DISABLE_ENTERPRISE_LOGIN=''
|
# ***** Cookies *****
|
||||||
REGISTER_CONVERSION_COOKIE_NAME=null
|
REGISTER_CONVERSION_COOKIE_NAME=null
|
||||||
ENABLE_PROGRESSIVE_PROFILING=''
|
USER_SURVEY_COOKIE_NAME=null
|
||||||
|
# ***** Links *****
|
||||||
|
LOGIN_ISSUE_SUPPORT_LINK=''
|
||||||
|
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null
|
||||||
|
# ***** Features flags *****
|
||||||
|
DISABLE_ENTERPRISE_LOGIN=''
|
||||||
|
ENABLE_DYNAMIC_REGISTRATION_FIELDS=''
|
||||||
|
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN=''
|
||||||
|
ENABLE_PERSONALIZED_RECOMMENDATIONS=''
|
||||||
MARKETING_EMAILS_OPT_IN=''
|
MARKETING_EMAILS_OPT_IN=''
|
||||||
ENABLE_COPPA_COMPLIANCE=''
|
SHOW_CONFIGURABLE_EDX_FIELDS=''
|
||||||
SHOW_DYNAMIC_PROFILING_PAGE=''
|
# ***** Zendesk related keys *****
|
||||||
|
ZENDESK_KEY=''
|
||||||
|
ZENDESK_LOGO_URL=''
|
||||||
|
# ***** Miscellaneous *****
|
||||||
|
APP_ID=''
|
||||||
|
MFE_CONFIG_API_URL=''
|
||||||
|
|||||||
@@ -18,16 +18,20 @@ ORDER_HISTORY_URL='http://localhost:1996/orders'
|
|||||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||||
SEGMENT_KEY=''
|
SEGMENT_KEY=''
|
||||||
SITE_NAME='Your Platform Name Here'
|
SITE_NAME='Your Platform Name Here'
|
||||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
INFO_EMAIL='info@example.com'
|
||||||
AUTHN_MINIMAL_HEADER=true
|
# ***** Cookies *****
|
||||||
LOGIN_ISSUE_SUPPORT_LINK='/login-issue-support-url'
|
|
||||||
TOS_AND_HONOR_CODE='http://localhost:18000/honor'
|
|
||||||
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'
|
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
|
||||||
ENABLE_COPPA_COMPLIANCE=''
|
SESSION_COOKIE_DOMAIN='localhost'
|
||||||
MARKETING_EMAILS_OPT_IN=''
|
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||||
|
USER_SURVEY_COOKIE_NAME='openedx-user-survey-type'
|
||||||
|
# ***** 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'
|
||||||
|
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK='http://localhost:1999/welcome'
|
||||||
|
# ***** 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'
|
||||||
@@ -16,11 +16,7 @@ ORDER_HISTORY_URL='http://localhost:1996/orders'
|
|||||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||||
SEGMENT_KEY=''
|
SEGMENT_KEY=''
|
||||||
SITE_NAME='Your Platform Name Here'
|
SITE_NAME='Your Platform Name Here'
|
||||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
|
||||||
LOGIN_ISSUE_SUPPORT_LINK='https://login-issue-support-url.com'
|
|
||||||
USER_SURVEY_COOKIE_NAME='openedx-user-survey-type'
|
USER_SURVEY_COOKIE_NAME='openedx-user-survey-type'
|
||||||
WELCOME_PAGE_SUPPORT_LINK='http://localhost:1999/welcome'
|
|
||||||
DISABLE_ENTERPRISE_LOGIN=''
|
|
||||||
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
|
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
|
||||||
MARKETING_EMAILS_OPT_IN=''
|
APP_ID=''
|
||||||
ENABLE_COPPA_COMPLIANCE=''
|
MFE_CONFIG_API_URL=''
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
coverage/*
|
coverage/*
|
||||||
dist/
|
dist/
|
||||||
|
docs
|
||||||
node_modules/
|
node_modules/
|
||||||
__mocks__/
|
__mocks__/
|
||||||
__snapshots__/
|
__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');
|
const { createConfig } = require('@edx/frontend-build');
|
||||||
|
|
||||||
module.exports = createConfig('eslint', {
|
module.exports = createConfig('eslint', {
|
||||||
rules: {
|
rules: {
|
||||||
// Temporarily update the 'indent', 'template-curly-spacing' and
|
// Temporarily update the 'indent', 'template-curly-spacing' and
|
||||||
// 'no-multiple-empty-lines' rules since they are causing eslint
|
// '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:
|
// @edx/frontend-build from v3 to v5:
|
||||||
// - TypeError: Cannot read property 'range' of null
|
// - TypeError: Cannot read property 'range' of null
|
||||||
'indent': [
|
indent: [
|
||||||
'error',
|
'error',
|
||||||
2,
|
2,
|
||||||
{ 'ignoredNodes': ['TemplateLiteral', 'SwitchCase'] }
|
{ ignoredNodes: ['TemplateLiteral', 'SwitchCase'] },
|
||||||
],
|
],
|
||||||
'template-curly-spacing': 'off',
|
'template-curly-spacing': 'off',
|
||||||
'jsx-a11y/label-has-associated-control': ['error', {
|
'jsx-a11y/label-has-associated-control': ['error', {
|
||||||
@@ -18,7 +19,34 @@ module.exports = createConfig('eslint', {
|
|||||||
labelAttributes: [],
|
labelAttributes: [],
|
||||||
controlComponents: [],
|
controlComponents: [],
|
||||||
assert: 'htmlFor',
|
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:
|
secrets:
|
||||||
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||||
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
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:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node: [12, 14, 16]
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
- name: Setup Nodejs Env
|
||||||
|
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||||
- name: Setup Nodejs
|
- name: Setup Nodejs
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ env.NODE_VER }}
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -41,8 +40,5 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Verify Es5
|
|
||||||
run: npm run is-es5
|
|
||||||
|
|
||||||
- name: Run Code Coverage
|
- 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:
|
jobs:
|
||||||
commitlint:
|
commitlint:
|
||||||
uses: edx/.github/.github/workflows/commitlint.yml@master
|
uses: openedx/.github/.github/workflows/commitlint.yml@master
|
||||||
|
|||||||
13
.github/workflows/lockfileversion-check.yml
vendored
Normal file
13
.github/workflows/lockfileversion-check.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#check package-lock file version
|
||||||
|
|
||||||
|
name: Lockfile Version check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
version-check:
|
||||||
|
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||||
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# This workflow runs when a comment is made on the ticket
|
||||||
|
# If the comment starts with "assign me" it assigns the author to the
|
||||||
|
# ticket (case insensitive)
|
||||||
|
|
||||||
|
name: Assign comment author to ticket if they say "assign me"
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
self_assign_by_comment:
|
||||||
|
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ node_modules
|
|||||||
npm-debug.log
|
npm-debug.log
|
||||||
coverage
|
coverage
|
||||||
module.config.js
|
module.config.js
|
||||||
|
.env.private
|
||||||
|
|
||||||
dist/
|
dist/
|
||||||
src/i18n/transifex_input.json
|
src/i18n/transifex_input.json
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
# The following users are the owners of all frontend-app-authn files
|
# The following users are the owners of all frontend-app-authn files
|
||||||
* @edx/vanguards
|
* @openedx/vanguards
|
||||||
|
|||||||
33
Makefile
Executable file → Normal file
33
Makefile
Executable file → Normal file
@@ -1,6 +1,7 @@
|
|||||||
export TRANSIFEX_RESOURCE = frontend-app-authn
|
export TRANSIFEX_RESOURCE = frontend-app-authn
|
||||||
transifex_langs = "ar,fr,es_419,zh_CN,it_IT,pt_PT,de_DE,uk,ru,hi"
|
transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fr_CA,it_IT,pt_PT,de_DE"
|
||||||
|
|
||||||
|
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||||
i18n = ./src/i18n
|
i18n = ./src/i18n
|
||||||
transifex_input = $(i18n)/transifex_input.json
|
transifex_input = $(i18n)/transifex_input.json
|
||||||
@@ -42,11 +43,37 @@ push_translations:
|
|||||||
# Pushing comments to Transifex...
|
# Pushing comments to Transifex...
|
||||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||||
|
|
||||||
|
ifeq ($(OPENEDX_ATLAS_PULL),)
|
||||||
# Pulls translations from Transifex.
|
# Pulls translations from Transifex.
|
||||||
pull_translations:
|
pull_translations:
|
||||||
tx pull -f --mode reviewed --languages=$(transifex_langs)
|
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||||
|
else
|
||||||
|
# Experimental: OEP-58 Pulls translations using atlas
|
||||||
|
pull_translations:
|
||||||
|
rm -rf src/i18n/messages
|
||||||
|
mkdir src/i18n/messages
|
||||||
|
cd src/i18n/messages \
|
||||||
|
&& atlas pull --filter=$(transifex_langs) \
|
||||||
|
translations/paragon/src/i18n/messages:paragon \
|
||||||
|
translations/frontend-app-authn/src/i18n/messages:frontend-app-authn
|
||||||
|
|
||||||
# This target is used by CI.
|
$(intl_imports) paragon frontend-app-authn
|
||||||
|
endif
|
||||||
|
|
||||||
|
# This target is used by Travis.
|
||||||
validate-no-uncommitted-package-lock-changes:
|
validate-no-uncommitted-package-lock-changes:
|
||||||
# Checking for package-lock.json changes...
|
# Checking for package-lock.json changes...
|
||||||
git diff --exit-code package-lock.json
|
git diff --exit-code package-lock.json
|
||||||
|
|
||||||
|
.PHONY: validate
|
||||||
|
validate:
|
||||||
|
make validate-no-uncommitted-package-lock-changes
|
||||||
|
npm run i18n_extract
|
||||||
|
npm run lint -- --max-warnings 0
|
||||||
|
npm run test
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
.PHONY: validate.ci
|
||||||
|
validate.ci:
|
||||||
|
npm ci
|
||||||
|
make validate
|
||||||
|
|||||||
188
README.rst
188
README.rst
@@ -1,48 +1,190 @@
|
|||||||
|Build Status| |Codecov| |license|
|
|Build Status| |ci-badge| |Codecov| |semantic-release|
|
||||||
|
|
||||||
frontend-app-authn
|
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.
|
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
|
Installation
|
||||||
npm start # The server will run on port 1999
|
------------
|
||||||
|
|
||||||
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
|
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
|
.. |Build Status| image:: https://api.travis-ci.com/edx/frontend-app-authn.svg?branch=master
|
||||||
:target: https://travis-ci.com/edx/frontend-app-authn
|
:target: https://travis-ci.com/edx/frontend-app-authn
|
||||||
.. |Codecov| image:: https://img.shields.io/codecov/c/github/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
|
:target: https://codecov.io/gh/edx/frontend-app-authn
|
||||||
.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-app-authn.svg
|
.. |ci-badge| image:: https://github.com/openedx/edx-developer-docs/actions/workflows/ci.yml/badge.svg
|
||||||
:target: @edx/frontend-app-authn
|
: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
|
/ProfilePhotoUploader.jsx // supporting view
|
||||||
/data // Note: most files here are named with a plural, as they contain many of the things in question.
|
/data // Note: most files here are named with a plural, as they contain many of the things in question.
|
||||||
/actions.js
|
/actions.js
|
||||||
/constants.js
|
/mockedData.js
|
||||||
/reducers.js
|
/reducers.js
|
||||||
/sagas.js
|
/sagas.js
|
||||||
/selectors.js
|
/selectors.js
|
||||||
|
|||||||
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
|
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',
|
'src/index.jsx',
|
||||||
'MainApp.jsx',
|
'MainApp.jsx',
|
||||||
],
|
],
|
||||||
|
testEnvironment: 'jsdom',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
nick: Authn MFE
|
nick: Authn MFE
|
||||||
oeps: {}
|
oeps: {}
|
||||||
owner: edx/vanguards
|
owner: openedx/vanguards
|
||||||
openedx-release:
|
openedx-release:
|
||||||
maybe: true # Delete this "maybe" line when you have decided about Open edX inclusion.
|
|
||||||
ref: master
|
ref: master
|
||||||
|
|||||||
39415
package-lock.json
generated
39415
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
83
package.json
83
package.json
@@ -4,16 +4,14 @@
|
|||||||
"description": "Frontend application template",
|
"description": "Frontend application template",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/edx/frontend-app-authn.git"
|
"url": "git+https://github.com/openedx/frontend-app-authn.git"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"last 2 versions",
|
"extends @edx/browserslist-config"
|
||||||
"ie 11"
|
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "fedx-scripts webpack",
|
"build": "fedx-scripts webpack",
|
||||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
"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 .",
|
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||||
"start": "fedx-scripts webpack-dev-server --progress",
|
"start": "fedx-scripts webpack-dev-server --progress",
|
||||||
@@ -26,67 +24,60 @@
|
|||||||
},
|
},
|
||||||
"author": "edX",
|
"author": "edX",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"homepage": "https://github.com/edx/frontend-app-authn#readme",
|
"homepage": "https://github.com/openedx/frontend-app-authn#readme",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/edx/frontend-app-authn/issues"
|
"url": "https://github.com/openedx/frontend-app-authn/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
||||||
"@edx/frontend-component-cookie-policy-banner": "2.1.14",
|
"@edx/frontend-platform": "4.5.1",
|
||||||
"@edx/frontend-platform": "1.15.5",
|
"@edx/paragon": "20.40.2",
|
||||||
"@edx/paragon": "19.10.2",
|
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
"@fortawesome/free-brands-svg-icons": "6.4.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
"@fortawesome/react-fontawesome": "0.2.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
"@optimizely/react-sdk": "^2.9.1",
|
||||||
"@fortawesome/react-fontawesome": "0.1.18",
|
"@redux-devtools/extension": "3.2.5",
|
||||||
"classnames": "2.3.1",
|
"algoliasearch": "^4.14.3",
|
||||||
"clipboard": "2.0.10",
|
"classnames": "2.3.2",
|
||||||
"core-js": "3.21.1",
|
"core-js": "3.30.2",
|
||||||
"extract-react-intl-messages": "4.1.1",
|
"fastest-levenshtein": "1.0.16",
|
||||||
"fastest-levenshtein": "1.0.12",
|
"form-urlencoded": "6.1.0",
|
||||||
"form-urlencoded": "4.2.1",
|
|
||||||
"formik": "2.2.9",
|
|
||||||
"lodash.camelcase": "4.3.0",
|
|
||||||
"lodash.snakecase": "4.1.1",
|
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"query-string": "5.1.1",
|
"query-string": "7.1.3",
|
||||||
"react": "16.14.0",
|
"react": "16.14.0",
|
||||||
"react-dom": "16.14.0",
|
"react-dom": "16.14.0",
|
||||||
"react-helmet": "6.1.0",
|
"react-helmet": "6.1.0",
|
||||||
"react-loading-skeleton": "2.2.0",
|
"react-loading-skeleton": "3.2.1",
|
||||||
"react-onclickoutside": "6.12.1",
|
"react-redux": "7.2.9",
|
||||||
"react-redux": "7.2.6",
|
|
||||||
"react-responsive": "8.2.0",
|
"react-responsive": "8.2.0",
|
||||||
"react-router": "5.2.1",
|
"react-router": "5.3.4",
|
||||||
"react-router-dom": "5.3.0",
|
"react-router-dom": "5.3.4",
|
||||||
"redux": "4.1.2",
|
"react-zendesk": "^0.1.13",
|
||||||
"@redux-devtools/extension": "3.2.2",
|
"redux": "4.2.0",
|
||||||
"redux-logger": "3.0.6",
|
"redux-logger": "3.0.6",
|
||||||
"redux-mock-store": "1.5.4",
|
"redux-mock-store": "1.5.4",
|
||||||
"redux-saga": "1.1.3",
|
"redux-saga": "1.2.3",
|
||||||
"redux-thunk": "2.4.1",
|
"redux-thunk": "2.4.2",
|
||||||
"regenerator-runtime": "0.13.9",
|
"regenerator-runtime": "0.13.11",
|
||||||
"reselect": "4.1.5",
|
"reselect": "4.1.8",
|
||||||
"sanitize-html": "2.7.0",
|
|
||||||
"semver-regex": "3.1.3",
|
|
||||||
"universal-cookie": "4.0.4"
|
"universal-cookie": "4.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@edx/frontend-build": "9.1.2",
|
"@edx/browserslist-config": "^1.1.1",
|
||||||
"@edx/reactifex": "1.0.3",
|
"@edx/frontend-build": "12.8.38",
|
||||||
"babel-plugin-formatjs": "10.3.18",
|
"@edx/reactifex": "1.1.0",
|
||||||
"codecov": "3.8.2",
|
"babel-plugin-formatjs": "10.5.1",
|
||||||
"enzyme": "3.11.0",
|
"enzyme": "3.11.0",
|
||||||
"enzyme-adapter-react-16": "1.15.6",
|
"enzyme-adapter-react-16": "1.15.7",
|
||||||
"es-check": "6.2.1",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"glob": "7.2.0",
|
"glob": "7.2.3",
|
||||||
"history": "5.3.0",
|
"history": "5.3.0",
|
||||||
"husky": "7.0.4",
|
"husky": "7.0.4",
|
||||||
"jest": "27.5.1",
|
"jest": "29.5.0",
|
||||||
"react-test-renderer": "16.14.0"
|
"react-test-renderer": "16.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en-us">
|
<html lang="en-us">
|
||||||
<head>
|
<head>
|
||||||
<title>Authn | edX</title>
|
<title>Authn | <%= process.env.SITE_NAME %></title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
|
||||||
<% if (process.env.OPTIMIZELY_URL) { %>
|
|
||||||
<script
|
|
||||||
src="<%= process.env.OPTIMIZELY_URL %>"
|
|
||||||
></script>
|
|
||||||
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
|
|
||||||
<script
|
|
||||||
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
|
|
||||||
></script>
|
|
||||||
<% } %>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,26 +1,39 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { AppProvider } from '@edx/frontend-platform/react';
|
import { AppProvider } from '@edx/frontend-platform/react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
UnAuthOnlyRoute, registerIcons, NotFoundPage, Logistration,
|
NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk,
|
||||||
} from './common-components';
|
} 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 configureStore from './data/configureStore';
|
||||||
|
import {
|
||||||
|
AUTHN_PROGRESSIVE_PROFILING,
|
||||||
|
LOGIN_PAGE,
|
||||||
|
PAGE_NOT_FOUND,
|
||||||
|
PASSWORD_RESET_CONFIRM,
|
||||||
|
RECOMMENDATIONS,
|
||||||
|
REGISTER_PAGE,
|
||||||
|
RESET_PAGE,
|
||||||
|
} from './data/constants';
|
||||||
import { updatePathWithQueryParams } from './data/utils';
|
import { updatePathWithQueryParams } from './data/utils';
|
||||||
import ForgotPasswordPage from './forgot-password';
|
import { ForgotPasswordPage } from './forgot-password';
|
||||||
import ResetPasswordPage from './reset-password';
|
import Logistration from './logistration/Logistration';
|
||||||
import WelcomePage, { ProgressiveProfiling } from './welcome';
|
import { ProgressiveProfiling } from './progressive-profiling';
|
||||||
|
import { RecommendationsPage } from './recommendations';
|
||||||
|
import { ResetPasswordPage } from './reset-password';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
registerIcons();
|
registerIcons();
|
||||||
|
|
||||||
const MainApp = () => (
|
const MainApp = () => (
|
||||||
<AppProvider store={configureStore()}>
|
<AppProvider store={configureStore()}>
|
||||||
|
<Helmet>
|
||||||
|
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||||
|
</Helmet>
|
||||||
|
{getConfig().ZENDESK_KEY && <Zendesk />}
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/">
|
<Route exact path="/">
|
||||||
<Redirect to={updatePathWithQueryParams(REGISTER_PAGE)} />
|
<Redirect to={updatePathWithQueryParams(REGISTER_PAGE)} />
|
||||||
@@ -29,11 +42,8 @@ const MainApp = () => (
|
|||||||
<UnAuthOnlyRoute exact path={REGISTER_PAGE} component={Logistration} />
|
<UnAuthOnlyRoute exact path={REGISTER_PAGE} component={Logistration} />
|
||||||
<UnAuthOnlyRoute exact path={RESET_PAGE} component={ForgotPasswordPage} />
|
<UnAuthOnlyRoute exact path={RESET_PAGE} component={ForgotPasswordPage} />
|
||||||
<Route exact path={PASSWORD_RESET_CONFIRM} component={ResetPasswordPage} />
|
<Route exact path={PASSWORD_RESET_CONFIRM} component={ResetPasswordPage} />
|
||||||
<Route
|
<Route exact path={AUTHN_PROGRESSIVE_PROFILING} component={ProgressiveProfiling} />
|
||||||
exact
|
<Route exact path={RECOMMENDATIONS} component={RecommendationsPage} />
|
||||||
path={WELCOME_PAGE}
|
|
||||||
component={(getConfig().SHOW_DYNAMIC_PROFILING_PAGE) ? ProgressiveProfiling : WelcomePage}
|
|
||||||
/>
|
|
||||||
<Route path={PAGE_NOT_FOUND} component={NotFoundPage} />
|
<Route path={PAGE_NOT_FOUND} component={NotFoundPage} />
|
||||||
<Route path="*">
|
<Route path="*">
|
||||||
<Redirect to={PAGE_NOT_FOUND} />
|
<Redirect to={PAGE_NOT_FOUND} />
|
||||||
|
|||||||
883
src/_style.scss
883
src/_style.scss
@@ -1,883 +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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stateful-button-variation1-width {
|
|
||||||
width: 16.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-button-width {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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-top: 1rem;
|
|
||||||
margin-left: 3px;
|
|
||||||
}
|
|
||||||
.suggested-username {
|
|
||||||
position: relative;
|
|
||||||
margin-top: -8.7%;
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
.suggested-username-close-button {
|
|
||||||
right: 0;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
49
src/base-component/AuthLargeLayout.jsx
Normal file
49
src/base-component/AuthLargeLayout.jsx
Normal file
@@ -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 AuthLargeLayout = ({ 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AuthLargeLayout.propTypes = {
|
||||||
|
username: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthLargeLayout;
|
||||||
@@ -1,69 +1,52 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import {
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
Col, Hyperlink, Image, Row,
|
import { Hyperlink, Image } from '@edx/paragon';
|
||||||
} from '@edx/paragon';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const AuthMediumLayout = (props) => {
|
const AuthMediumLayout = ({ username }) => {
|
||||||
const { intl, username } = props;
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container row p-0 mb-3 medium-container">
|
<>
|
||||||
<div className="col-md-10 p-0 screen-header-light">
|
<div className="w-100 medium-screen-top-stripe" />
|
||||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
<div className="w-100 p-0 mb-3 d-flex">
|
||||||
<Image alt={getConfig().SITE_NAME} className="logo" src={getConfig().LOGO_WHITE_URL} />
|
<div className="col-md-10 bg-light-200">
|
||||||
</Hyperlink>
|
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||||
<div className="d-flex align-items-center justify-content-center ml-6">
|
<Image className="logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
|
||||||
<div>
|
</Hyperlink>
|
||||||
<Row>
|
<div className="d-flex align-items-center justify-content-center mb-4 ml-5">
|
||||||
<Col xs={3}>
|
<div className="medium-yellow-line mt-5 mr-n2" />
|
||||||
<svg className="medium-svg-line ml-5 mt-5">
|
<div>
|
||||||
<line x1="60" y1="0" x2="5" y2="220" />
|
<h1 className="h3 data-hj-suppress mw-320">
|
||||||
</svg>
|
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
|
||||||
</Col>
|
</h1>
|
||||||
<Col xs={9}>
|
<h2 className="display-1">
|
||||||
<h3 className="data-hj-suppress">
|
{formatMessage(messages['complete.your.profile.1'])}
|
||||||
{intl.formatMessage(
|
<div className="text-accent-a">
|
||||||
messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username },
|
{formatMessage(messages['complete.your.profile.2'])}
|
||||||
)}
|
|
||||||
</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>
|
</div>
|
||||||
</Col>
|
</h2>
|
||||||
</Row>
|
</div>
|
||||||
</div>
|
</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>
|
</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 = {
|
AuthMediumLayout.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
username: PropTypes.string.isRequired,
|
username: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(AuthMediumLayout);
|
export default AuthMediumLayout;
|
||||||
|
|||||||
@@ -1,68 +1,41 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import { Hyperlink, Image } from '@edx/paragon';
|
||||||
Col, Hyperlink, Image, Row,
|
import PropTypes from 'prop-types';
|
||||||
} from '@edx/paragon';
|
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const AuthSmallLayout = (props) => {
|
const AuthSmallLayout = ({ username }) => {
|
||||||
const { intl, username, variant } = props;
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="small-screen-header-light">
|
<div className="min-vw-100 bg-light-200">
|
||||||
|
<div className="col-md-12 small-screen-top-stripe" />
|
||||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||||
<Image alt={getConfig().SITE_NAME} className="logo" src={getConfig().LOGO_WHITE_URL} />
|
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
<div className={classNames('d-flex mt-3', { 'pl-6': variant === 'sm' })}>
|
<div className="d-flex align-items-center mb-3 mt-3 mr-3">
|
||||||
|
<div className="small-yellow-line mt-4.5" />
|
||||||
<div>
|
<div>
|
||||||
<Row>
|
<h1 className="h5 data-hj-suppress">
|
||||||
<Col xs={3}>
|
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
|
||||||
<svg className={classNames(
|
</h1>
|
||||||
'mt-4\.5', // eslint-disable-line no-useless-escape
|
<h2 className="h1">
|
||||||
{
|
{formatMessage(messages['complete.your.profile.1'])}
|
||||||
'extra-small-svg-line': variant === 'xs',
|
<div className="text-accent-a">
|
||||||
'small-svg-line': variant === 'sm',
|
{formatMessage(messages['complete.your.profile.2'])}
|
||||||
},
|
</div>
|
||||||
)}
|
</h2>
|
||||||
>
|
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
AuthSmallLayout.defaultProps = {
|
|
||||||
variant: 'sm',
|
|
||||||
};
|
|
||||||
|
|
||||||
AuthSmallLayout.propTypes = {
|
AuthSmallLayout.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
username: PropTypes.string.isRequired,
|
username: PropTypes.string.isRequired,
|
||||||
variant: PropTypes.oneOf(['sm', 'xs']),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(AuthSmallLayout);
|
export default AuthSmallLayout;
|
||||||
|
|||||||
@@ -1,79 +1,34 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
import MediaQuery from 'react-responsive';
|
|
||||||
import { breakpoints } from '@edx/paragon';
|
import { breakpoints } from '@edx/paragon';
|
||||||
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
|
import classNames from 'classnames';
|
||||||
import { getLocale } from '@edx/frontend-platform/i18n';
|
import PropTypes from 'prop-types';
|
||||||
|
import MediaQuery from 'react-responsive';
|
||||||
|
|
||||||
|
import AuthLargeLayout from './AuthLargeLayout';
|
||||||
|
import AuthMediumLayout from './AuthMediumLayout';
|
||||||
|
import AuthSmallLayout from './AuthSmallLayout';
|
||||||
import LargeLayout from './LargeLayout';
|
import LargeLayout from './LargeLayout';
|
||||||
import MediumLayout from './MediumLayout';
|
import MediumLayout from './MediumLayout';
|
||||||
import SmallLayout from './SmallLayout';
|
import SmallLayout from './SmallLayout';
|
||||||
|
|
||||||
import AuthExtraLargeLayout from './AuthExtraLargeLayout';
|
const BaseComponent = ({ children, showWelcomeBanner }) => {
|
||||||
import AuthMediumLayout from './AuthMediumLayout';
|
|
||||||
import AuthSmallLayout from './AuthSmallLayout';
|
|
||||||
import DiscountExperimentBanner from './DiscountBanner';
|
|
||||||
|
|
||||||
const BaseComponent = ({ children, isRegistrationPage, showWelcomeBanner }) => {
|
|
||||||
const authenticatedUser = showWelcomeBanner ? getAuthenticatedUser() : null;
|
const authenticatedUser = showWelcomeBanner ? getAuthenticatedUser() : null;
|
||||||
const [optimizelyExperimentName, setOptimizelyExperimentName] = useState('');
|
const username = authenticatedUser ? authenticatedUser.username : null;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const { experimentName } = window;
|
|
||||||
|
|
||||||
if (experimentName) {
|
|
||||||
setOptimizelyExperimentName(experimentName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isRegistrationPage && optimizelyExperimentName === 'variation2' ? <DiscountExperimentBanner /> : null}
|
<div className="col-md-12 extra-large-screen-top-stripe" />
|
||||||
<CookiePolicyBanner languageCode={getLocale()} />
|
<div className="layout">
|
||||||
<MediaQuery minWidth={breakpoints.extraLarge.minWidth} maxWidth={breakpoints.extraLarge.maxWidth}>
|
<MediaQuery maxWidth={breakpoints.small.maxWidth - 1}>
|
||||||
<div className="col-md-12 extra-large-screen-top-stripe" />
|
{authenticatedUser ? <AuthSmallLayout username={username} /> : <SmallLayout />}
|
||||||
</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>
|
||||||
<MediaQuery minWidth={breakpoints.small.minWidth} maxWidth={breakpoints.small.maxWidth}>
|
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
|
||||||
<div className="col-md-12 small-screen-top-stripe" />
|
{authenticatedUser ? <AuthMediumLayout username={username} /> : <MediumLayout />}
|
||||||
{authenticatedUser ? <AuthSmallLayout username={authenticatedUser.username} /> : (
|
|
||||||
<SmallLayout experimentName={optimizelyExperimentName} isRegistrationPage={isRegistrationPage} />
|
|
||||||
)}
|
|
||||||
</MediaQuery>
|
</MediaQuery>
|
||||||
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.medium.maxWidth}>
|
<MediaQuery minWidth={breakpoints.extraLarge.minWidth} maxWidth={breakpoints.extraExtraLarge.maxWidth}>
|
||||||
<div className="w-100 medium-screen-top-stripe" />
|
{authenticatedUser ? <AuthLargeLayout username={username} /> : <LargeLayout />}
|
||||||
{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>
|
</MediaQuery>
|
||||||
|
|
||||||
<div className={classNames('content', { 'align-items-center mt-0': authenticatedUser })}>
|
<div className={classNames('content', { 'align-items-center mt-0': authenticatedUser })}>
|
||||||
@@ -85,13 +40,11 @@ const BaseComponent = ({ children, isRegistrationPage, showWelcomeBanner }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
BaseComponent.defaultProps = {
|
BaseComponent.defaultProps = {
|
||||||
isRegistrationPage: false,
|
|
||||||
showWelcomeBanner: false,
|
showWelcomeBanner: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
BaseComponent.propTypes = {
|
BaseComponent.propTypes = {
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
isRegistrationPage: PropTypes.bool,
|
|
||||||
showWelcomeBanner: PropTypes.bool,
|
showWelcomeBanner: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 +1,45 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Hyperlink, Image } from '@edx/paragon';
|
import { Hyperlink, Image } from '@edx/paragon';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import LargeScreenLeftLayout from './LargeLeftLayout';
|
import messages from './messages';
|
||||||
|
|
||||||
const LargeLayout = ({ experimentName, isRegistrationPage }) => (
|
const LargeLayout = () => {
|
||||||
<div className="container row p-0 m-0 large-screen-container">
|
const { formatMessage } = useIntl();
|
||||||
<div className="col-md-9 p-0 screen-header-primary">
|
|
||||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
return (
|
||||||
<Image alt={getConfig().SITE_NAME} className="logo position-absolute" src={getConfig().LOGO_WHITE_URL} />
|
<div className="w-50 d-flex">
|
||||||
</Hyperlink>
|
<div className="col-md-9 bg-primary-400">
|
||||||
<LargeScreenLeftLayout experimentName={experimentName} isRegistrationPage={isRegistrationPage} />
|
<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>
|
</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;
|
export default LargeLayout;
|
||||||
|
|||||||
@@ -1,84 +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 pr-0 mt-lg-n2 d-flex align-items-center">
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
LargeLeftLayout.propTypes = {
|
|
||||||
intl: intlShape.isRequired,
|
|
||||||
experimentName: PropTypes.string,
|
|
||||||
isRegistrationPage: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
LargeLeftLayout.defaultProps = {
|
|
||||||
experimentName: '',
|
|
||||||
isRegistrationPage: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(LargeLeftLayout);
|
|
||||||
@@ -1,102 +1,50 @@
|
|||||||
import React, { useState } from 'react';
|
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 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 messages from './messages';
|
||||||
import SideDiscountBanner from './SideDiscountBanner';
|
|
||||||
|
|
||||||
const MediumLayout = (props) => {
|
const MediumLayout = () => {
|
||||||
const { intl, isRegistrationPage, experimentName } = props;
|
const { formatMessage } = useIntl();
|
||||||
const [showToast, setToastShow] = useState(false);
|
|
||||||
new ClipboardJS('.copyIcon'); // eslint-disable-line no-new
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(
|
<>
|
||||||
'container row p-0 mb-3 medium-screen-container',
|
<div className="w-100 medium-screen-top-stripe" />
|
||||||
{
|
<div className="w-100 p-0 mb-3 d-flex">
|
||||||
'variation1-medium-screen': experimentName === 'variation1' && isRegistrationPage,
|
<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>
|
||||||
<Toast
|
<div className="d-flex align-items-center justify-content-center mb-4 ">
|
||||||
onClose={() => setToastShow(false)}
|
<div className={classNames({ 'mt-1 medium-yellow-line': getConfig().SITE_NAME === 'edX' })} />
|
||||||
show={showToast}
|
<div>
|
||||||
>
|
<h1
|
||||||
{intl.formatMessage(messages['code.copied'])}
|
className={classNames(
|
||||||
</Toast>
|
'display-1 text-white mt-5 mb-5 mr-2',
|
||||||
<div className="col-md-10 p-0 screen-header-primary">
|
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' },
|
||||||
<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 />
|
<span className="mr-2">{formatMessage(messages['start.learning'])}</span>
|
||||||
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
<span className="text-accent-a d-inline-block">
|
||||||
</span>
|
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||||
</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>
|
</span>
|
||||||
</div>
|
</h1>
|
||||||
) : null}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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 = {
|
export default MediumLayout;
|
||||||
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 +1,39 @@
|
|||||||
import React, { useState } from 'react';
|
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 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 messages from './messages';
|
||||||
import SideDiscountBanner from './SideDiscountBanner';
|
|
||||||
|
|
||||||
const SmallLayout = (props) => {
|
const SmallLayout = () => {
|
||||||
const { intl, isRegistrationPage, experimentName } = props;
|
const { formatMessage } = useIntl();
|
||||||
const [showToast, setToastShow] = useState(false);
|
|
||||||
new ClipboardJS('.copyIcon'); // eslint-disable-line no-new
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<span className="bg-primary-400 w-100">
|
||||||
<div className="small-screen-header-primary">
|
<div className="col-md-12 small-screen-top-stripe" />
|
||||||
<Toast
|
<div>
|
||||||
onClose={() => setToastShow(false)}
|
|
||||||
show={showToast}
|
|
||||||
>
|
|
||||||
{intl.formatMessage(messages['code.copied'])}
|
|
||||||
</Toast>
|
|
||||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||||
<Image alt={getConfig().SITE_NAME} className="logo" src={getConfig().LOGO_WHITE_URL} />
|
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
<div className="d-flex mt-3">
|
<div className="d-flex align-items-center mb-3 mt-3 mr-3">
|
||||||
<svg className={classNames(
|
<div className={classNames({ 'small-yellow-line mr-n2.5': getConfig().SITE_NAME === 'edX' })} />
|
||||||
'small-screen-svg-line',
|
<h1
|
||||||
{
|
className={classNames(
|
||||||
'variation1-bar-color': experimentName === 'variation1' && isRegistrationPage,
|
'text-white mt-3.5 mb-3.5',
|
||||||
'variation2-bar-color': experimentName === 'variation2' && isRegistrationPage,
|
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' },
|
||||||
},
|
)}
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<line x1="55" y1="0" x2="40" y2="65" />
|
<span className="mr-1">{formatMessage(messages['start.learning'])}</span>
|
||||||
</svg>
|
<span className="text-accent-a d-inline-block">
|
||||||
<div className="pb-3">
|
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||||
<h1 className="small-heading">
|
</span>
|
||||||
{intl.formatMessage(messages['start.learning'])}
|
</h1>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
SmallLayout.propTypes = {
|
export default SmallLayout;
|
||||||
intl: intlShape.isRequired,
|
|
||||||
experimentName: PropTypes.string,
|
|
||||||
isRegistrationPage: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
SmallLayout.defaultProps = {
|
|
||||||
experimentName: '',
|
|
||||||
isRegistrationPage: false,
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(SmallLayout);
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export { default } from './BaseComponent';
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
export { default as BaseComponent } from './BaseComponent';
|
||||||
|
|||||||
@@ -11,11 +11,6 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'with {siteName}',
|
defaultMessage: 'with {siteName}',
|
||||||
description: 'Header text with site name for logistration MFE pages',
|
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
|
// authenticated user base component text
|
||||||
'complete.your.profile.1': {
|
'complete.your.profile.1': {
|
||||||
id: 'complete.your.profile.1',
|
id: 'complete.your.profile.1',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
|
||||||
import LargeLayout from '../LargeLayout';
|
import LargeLayout from '../LargeLayout';
|
||||||
import MediumLayout from '../MediumLayout';
|
import MediumLayout from '../MediumLayout';
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Form, Button,
|
Button, Form,
|
||||||
|
Icon,
|
||||||
} from '@edx/paragon';
|
} from '@edx/paragon';
|
||||||
|
import { Login } from '@edx/paragon/icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component renders the Single sign-on (SSO) button only for the tpa provider passed
|
||||||
|
* */
|
||||||
const EnterpriseSSO = (props) => {
|
const EnterpriseSSO = (props) => {
|
||||||
const { intl } = props;
|
const { formatMessage } = useIntl();
|
||||||
const tpaProvider = props.provider;
|
const tpaProvider = props.provider;
|
||||||
|
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
|
||||||
|
|
||||||
const handleSubmit = (e, url) => {
|
const handleSubmit = (e, url) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -33,7 +37,7 @@ const EnterpriseSSO = (props) => {
|
|||||||
<div className="d-flex flex-column">
|
<div className="d-flex flex-column">
|
||||||
<div className="mw-450">
|
<div className="mw-450">
|
||||||
<Form className="m-0">
|
<Form className="m-0">
|
||||||
<p>{intl.formatMessage(messages['enterprisetpa.title.heading'], { providerName: tpaProvider.name })}</p>
|
<p>{formatMessage(messages['enterprisetpa.title.heading'], { providerName: tpaProvider.name })}</p>
|
||||||
<Button
|
<Button
|
||||||
id={tpaProvider.id}
|
id={tpaProvider.id}
|
||||||
key={tpaProvider.id}
|
key={tpaProvider.id}
|
||||||
@@ -44,16 +48,18 @@ const EnterpriseSSO = (props) => {
|
|||||||
>
|
>
|
||||||
{tpaProvider.iconImage ? (
|
{tpaProvider.iconImage ? (
|
||||||
<div aria-hidden="true">
|
<div aria-hidden="true">
|
||||||
<img className="icon-image" src={tpaProvider.iconImage} alt={`icon ${tpaProvider.name}`} />
|
<img className="btn-tpa__image-icon" src={tpaProvider.iconImage} alt={`icon ${tpaProvider.name}`} />
|
||||||
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
|
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<>
|
<>
|
||||||
<div className="font-container" aria-hidden="true">
|
<div className="btn-tpa__font-container" aria-hidden="true">
|
||||||
<FontAwesomeIcon
|
{SUPPORTED_ICON_CLASSES.includes(tpaProvider.iconClass) ? (
|
||||||
icon={SUPPORTED_ICON_CLASSES.includes(tpaProvider.iconClass) ? ['fab', tpaProvider.iconClass] : faSignInAlt}
|
<FontAwesomeIcon icon={['fab', tpaProvider.iconClass]} />)
|
||||||
/>
|
: (
|
||||||
|
<Icon className="h-75" src={Login} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
|
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
|
||||||
</>
|
</>
|
||||||
@@ -62,12 +68,15 @@ const EnterpriseSSO = (props) => {
|
|||||||
<div className="mb-4" />
|
<div className="mb-4" />
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
id="other-ways-to-sign-in"
|
||||||
variant="outline-primary"
|
variant="outline-primary"
|
||||||
state="Complete"
|
state="Complete"
|
||||||
className="w-100"
|
className="w-100"
|
||||||
onClick={(e) => handleClick(e)}
|
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>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,7 +107,6 @@ EnterpriseSSO.propTypes = {
|
|||||||
loginUrl: PropTypes.string,
|
loginUrl: PropTypes.string,
|
||||||
registerUrl: PropTypes.string,
|
registerUrl: PropTypes.string,
|
||||||
}),
|
}),
|
||||||
intl: intlShape.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(EnterpriseSSO);
|
export default EnterpriseSSO;
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ const FormGroup = (props) => {
|
|||||||
as={props.as}
|
as={props.as}
|
||||||
readOnly={props.readOnly}
|
readOnly={props.readOnly}
|
||||||
type={props.type}
|
type={props.type}
|
||||||
className="form-field"
|
aria-invalid={props.errorMessage !== ''}
|
||||||
|
className="form-group__form-field"
|
||||||
autoComplete={props.autoComplete}
|
autoComplete={props.autoComplete}
|
||||||
|
spellCheck={props.spellCheck}
|
||||||
name={props.name}
|
name={props.name}
|
||||||
value={props.value}
|
value={props.value}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
@@ -35,7 +37,6 @@ const FormGroup = (props) => {
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onChange={props.handleChange}
|
onChange={props.handleChange}
|
||||||
controlClassName={props.borderClass}
|
controlClassName={props.borderClass}
|
||||||
|
|
||||||
trailingElement={props.trailingElement}
|
trailingElement={props.trailingElement}
|
||||||
floatingLabel={props.floatingLabel}
|
floatingLabel={props.floatingLabel}
|
||||||
>
|
>
|
||||||
@@ -63,41 +64,43 @@ const FormGroup = (props) => {
|
|||||||
|
|
||||||
FormGroup.defaultProps = {
|
FormGroup.defaultProps = {
|
||||||
as: 'input',
|
as: 'input',
|
||||||
errorMessage: '',
|
|
||||||
borderClass: '',
|
|
||||||
autoComplete: null,
|
autoComplete: null,
|
||||||
readOnly: false,
|
borderClass: '',
|
||||||
handleBlur: null,
|
|
||||||
handleChange: () => {},
|
|
||||||
handleFocus: null,
|
|
||||||
handleClick: null,
|
|
||||||
helpText: [],
|
|
||||||
options: null,
|
|
||||||
trailingElement: null,
|
|
||||||
type: 'text',
|
|
||||||
children: null,
|
children: null,
|
||||||
className: '',
|
className: '',
|
||||||
|
errorMessage: '',
|
||||||
|
handleBlur: null,
|
||||||
|
handleChange: () => {},
|
||||||
|
handleClick: null,
|
||||||
|
handleFocus: null,
|
||||||
|
helpText: [],
|
||||||
|
options: null,
|
||||||
|
readOnly: false,
|
||||||
|
spellCheck: null,
|
||||||
|
trailingElement: null,
|
||||||
|
type: 'text',
|
||||||
};
|
};
|
||||||
|
|
||||||
FormGroup.propTypes = {
|
FormGroup.propTypes = {
|
||||||
as: PropTypes.string,
|
as: PropTypes.string,
|
||||||
errorMessage: PropTypes.string,
|
|
||||||
borderClass: PropTypes.string,
|
|
||||||
autoComplete: PropTypes.string,
|
autoComplete: PropTypes.string,
|
||||||
readOnly: PropTypes.bool,
|
borderClass: PropTypes.string,
|
||||||
|
children: PropTypes.element,
|
||||||
|
className: PropTypes.string,
|
||||||
|
errorMessage: PropTypes.string,
|
||||||
floatingLabel: PropTypes.string.isRequired,
|
floatingLabel: PropTypes.string.isRequired,
|
||||||
handleBlur: PropTypes.func,
|
handleBlur: PropTypes.func,
|
||||||
handleChange: PropTypes.func,
|
handleChange: PropTypes.func,
|
||||||
handleFocus: PropTypes.func,
|
|
||||||
handleClick: PropTypes.func,
|
handleClick: PropTypes.func,
|
||||||
|
handleFocus: PropTypes.func,
|
||||||
helpText: PropTypes.arrayOf(PropTypes.string),
|
helpText: PropTypes.arrayOf(PropTypes.string),
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
options: PropTypes.func,
|
options: PropTypes.func,
|
||||||
|
readOnly: PropTypes.bool,
|
||||||
|
spellCheck: PropTypes.string,
|
||||||
trailingElement: PropTypes.element,
|
trailingElement: PropTypes.element,
|
||||||
type: PropTypes.string,
|
type: PropTypes.string,
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
children: PropTypes.element,
|
|
||||||
className: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FormGroup;
|
export default FormGroup;
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
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 { Button, Hyperlink, Icon } from '@edx/paragon';
|
||||||
import { Institution } from '@edx/paragon/icons';
|
import { Institution } from '@edx/paragon/icons';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component renders the Institution login button
|
||||||
|
* */
|
||||||
export const RenderInstitutionButton = props => {
|
export const RenderInstitutionButton = props => {
|
||||||
const { onSubmitHandler, buttonTitle } = props;
|
const { onSubmitHandler, buttonTitle } = props;
|
||||||
|
|
||||||
@@ -22,10 +27,13 @@ export const RenderInstitutionButton = props => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component renders the page list of available institutions for login
|
||||||
|
* */
|
||||||
const InstitutionLogistration = props => {
|
const InstitutionLogistration = props => {
|
||||||
const lmsBaseUrl = getConfig().LMS_BASE_URL;
|
const lmsBaseUrl = getConfig().LMS_BASE_URL;
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const {
|
const {
|
||||||
intl,
|
|
||||||
secondaryProviders,
|
secondaryProviders,
|
||||||
headingTitle,
|
headingTitle,
|
||||||
} = props;
|
} = props;
|
||||||
@@ -34,11 +42,11 @@ const InstitutionLogistration = props => {
|
|||||||
<>
|
<>
|
||||||
<div className="d-flex justify-content-left mb-4 mt-2">
|
<div className="d-flex justify-content-left mb-4 mt-2">
|
||||||
<div className="flex-column">
|
<div className="flex-column">
|
||||||
<h4 className="mb-2 font-weight-bold institute-heading">
|
<h4 className="mb-2 font-weight-bold institutions__heading">
|
||||||
{headingTitle}
|
{headingTitle}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="mb-2">
|
<p className="mb-2">
|
||||||
{intl.formatMessage(messages['institution.login.page.sub.heading'])}
|
{formatMessage(messages['institution.login.page.sub.heading'])}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,7 +57,7 @@ const InstitutionLogistration = props => {
|
|||||||
<tr key={provider} className="pgn__data-table-row">
|
<tr key={provider} className="pgn__data-table-row">
|
||||||
<td>
|
<td>
|
||||||
<Hyperlink
|
<Hyperlink
|
||||||
className="btn nav-item p-0 mb-1 secondary-provider-link"
|
className="btn nav-item p-0 mb-1 institutions--provider-link"
|
||||||
destination={lmsBaseUrl + provider.loginUrl}
|
destination={lmsBaseUrl + provider.loginUrl}
|
||||||
>
|
>
|
||||||
{provider.name}
|
{provider.name}
|
||||||
@@ -87,7 +95,6 @@ RenderInstitutionButton.defaultProps = {
|
|||||||
|
|
||||||
InstitutionLogistration.propTypes = {
|
InstitutionLogistration.propTypes = {
|
||||||
...LogistrationProps,
|
...LogistrationProps,
|
||||||
intl: intlShape.isRequired,
|
|
||||||
headingTitle: PropTypes.string,
|
headingTitle: PropTypes.string,
|
||||||
};
|
};
|
||||||
InstitutionLogistration.defaultProps = {
|
InstitutionLogistration.defaultProps = {
|
||||||
@@ -95,4 +102,4 @@ InstitutionLogistration.defaultProps = {
|
|||||||
headingTitle: '',
|
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 React from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
export default function NotFoundPage() {
|
const NotFoundPage = () => (
|
||||||
return (
|
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
||||||
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
<p className="my-0 py-5 text-muted mw-32em">
|
||||||
<p className="my-0 py-5 text-muted mw-32em">
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id="error.notfound.message"
|
||||||
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."
|
||||||
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"
|
||||||
description="error message when a page does not exist"
|
/>
|
||||||
/>
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
|
||||||
}
|
export default NotFoundPage;
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import {
|
||||||
Form, IconButton, useToggle, Tooltip, OverlayTrigger, Icon,
|
Form, Icon, IconButton, OverlayTrigger, Tooltip, useToggle,
|
||||||
} from '@edx/paragon';
|
} from '@edx/paragon';
|
||||||
import {
|
import {
|
||||||
Check, Remove, Visibility, VisibilityOff,
|
Check, Remove, Visibility, VisibilityOff,
|
||||||
} from '@edx/paragon/icons';
|
} from '@edx/paragon/icons';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
|
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
|
||||||
|
|
||||||
const PasswordField = (props) => {
|
const PasswordField = (props) => {
|
||||||
const { formatMessage } = props.intl;
|
const { formatMessage } = useIntl();
|
||||||
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
|
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
|
||||||
const [showTooltip, setShowTooltip] = useState(false);
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
|
|
||||||
@@ -30,24 +30,24 @@ const PasswordField = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const HideButton = (
|
const HideButton = (
|
||||||
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="passwordValidation" src={VisibilityOff} iconAs={Icon} onClick={setHiddenTrue} size="sm" variant="secondary" alt={formatMessage(messages['hide.password'])} />
|
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="password" src={VisibilityOff} iconAs={Icon} onClick={setHiddenTrue} size="sm" variant="secondary" alt={formatMessage(messages['hide.password'])} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const ShowButton = (
|
const ShowButton = (
|
||||||
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="passwordValidation" src={Visibility} iconAs={Icon} onClick={setHiddenFalse} size="sm" variant="secondary" alt={formatMessage(messages['show.password'])} />
|
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="password" src={Visibility} iconAs={Icon} onClick={setHiddenFalse} size="sm" variant="secondary" alt={formatMessage(messages['show.password'])} />
|
||||||
);
|
);
|
||||||
const placement = window.innerWidth < 768 ? 'top' : 'left';
|
const placement = window.innerWidth < 768 ? 'top' : 'left';
|
||||||
const tooltip = (
|
const tooltip = (
|
||||||
<Tooltip id={`password-requirement-${placement}`}>
|
<Tooltip id={`password-requirement-${placement}`}>
|
||||||
<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} />}
|
{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'])}
|
{formatMessage(messages['one.letter'])}
|
||||||
</span>
|
</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} />}
|
{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'])}
|
{formatMessage(messages['one.number'])}
|
||||||
</span>
|
</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} />}
|
{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'])}
|
{formatMessage(messages['eight.characters'])}
|
||||||
</span>
|
</span>
|
||||||
@@ -59,10 +59,12 @@ const PasswordField = (props) => {
|
|||||||
<OverlayTrigger key="tooltip" placement={placement} overlay={tooltip} show={showTooltip}>
|
<OverlayTrigger key="tooltip" placement={placement} overlay={tooltip} show={showTooltip}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as="input"
|
as="input"
|
||||||
className="form-field"
|
className="form-group__form-field"
|
||||||
type={isPasswordHidden ? 'password' : 'text'}
|
type={isPasswordHidden ? 'password' : 'text'}
|
||||||
name={props.name}
|
name={props.name}
|
||||||
value={props.value}
|
value={props.value}
|
||||||
|
autoComplete={props.autoComplete}
|
||||||
|
aria-invalid={props.errorMessage !== ''}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onChange={props.handleChange}
|
onChange={props.handleChange}
|
||||||
@@ -88,6 +90,7 @@ PasswordField.defaultProps = {
|
|||||||
handleFocus: null,
|
handleFocus: null,
|
||||||
handleChange: () => {},
|
handleChange: () => {},
|
||||||
showRequirements: true,
|
showRequirements: true,
|
||||||
|
autoComplete: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
PasswordField.propTypes = {
|
PasswordField.propTypes = {
|
||||||
@@ -97,10 +100,10 @@ PasswordField.propTypes = {
|
|||||||
handleBlur: PropTypes.func,
|
handleBlur: PropTypes.func,
|
||||||
handleFocus: PropTypes.func,
|
handleFocus: PropTypes.func,
|
||||||
handleChange: PropTypes.func,
|
handleChange: PropTypes.func,
|
||||||
intl: intlShape.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
showRequirements: PropTypes.bool,
|
showRequirements: PropTypes.bool,
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
|
autoComplete: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(PasswordField);
|
export default PasswordField;
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS } from '../data/constants';
|
||||||
|
|
||||||
import { WELCOME_PAGE } from '../data/constants';
|
|
||||||
import { setCookie } from '../data/utils';
|
import { setCookie } from '../data/utils';
|
||||||
|
|
||||||
function RedirectLogistration(props) {
|
const RedirectLogistration = (props) => {
|
||||||
const {
|
const {
|
||||||
finishAuthUrl, redirectUrl, redirectToWelcomePage, success,
|
finishAuthUrl,
|
||||||
|
redirectUrl,
|
||||||
|
redirectToProgressiveProfilingPage,
|
||||||
|
success,
|
||||||
|
optionalFields,
|
||||||
|
redirectToRecommendationsPage,
|
||||||
|
educationLevel,
|
||||||
|
userId,
|
||||||
} = props;
|
} = props;
|
||||||
let finalRedirectUrl = '';
|
let finalRedirectUrl = '';
|
||||||
|
|
||||||
@@ -25,31 +31,65 @@ function RedirectLogistration(props) {
|
|||||||
finalRedirectUrl = redirectUrl;
|
finalRedirectUrl = redirectUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redirectToWelcomePage) {
|
// Redirect to Progressive Profiling after successful registration
|
||||||
|
if (redirectToProgressiveProfilingPage) {
|
||||||
|
// TODO: Do we still need this cookie?
|
||||||
setCookie('van-504-returning-user', true);
|
setCookie('van-504-returning-user', true);
|
||||||
// use this component to redirect WelcomePage after successful registration
|
|
||||||
// return <Redirect to={WELCOME_PAGE} />;
|
|
||||||
const registrationResult = { redirectUrl: finalRedirectUrl, success };
|
const registrationResult = { redirectUrl: finalRedirectUrl, success };
|
||||||
return <Redirect to={{ pathname: WELCOME_PAGE, state: { registrationResult } }} />;
|
return (
|
||||||
|
<Redirect to={{
|
||||||
|
pathname: AUTHN_PROGRESSIVE_PROFILING,
|
||||||
|
state: {
|
||||||
|
registrationResult,
|
||||||
|
optionalFields,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to Recommendation page
|
||||||
|
if (redirectToRecommendationsPage) {
|
||||||
|
const registrationResult = { redirectUrl: finalRedirectUrl, success };
|
||||||
|
return (
|
||||||
|
<Redirect to={{
|
||||||
|
pathname: RECOMMENDATIONS,
|
||||||
|
state: {
|
||||||
|
registrationResult,
|
||||||
|
educationLevel,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.href = finalRedirectUrl;
|
window.location.href = finalRedirectUrl;
|
||||||
}
|
}
|
||||||
return <></>;
|
|
||||||
}
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
RedirectLogistration.defaultProps = {
|
RedirectLogistration.defaultProps = {
|
||||||
|
educationLevel: null,
|
||||||
finishAuthUrl: null,
|
finishAuthUrl: null,
|
||||||
success: false,
|
success: false,
|
||||||
redirectUrl: '',
|
redirectUrl: '',
|
||||||
redirectToWelcomePage: false,
|
redirectToProgressiveProfilingPage: false,
|
||||||
|
optionalFields: {},
|
||||||
|
redirectToRecommendationsPage: false,
|
||||||
|
userId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
RedirectLogistration.propTypes = {
|
RedirectLogistration.propTypes = {
|
||||||
|
educationLevel: PropTypes.string,
|
||||||
finishAuthUrl: PropTypes.string,
|
finishAuthUrl: PropTypes.string,
|
||||||
success: PropTypes.bool,
|
success: PropTypes.bool,
|
||||||
redirectUrl: PropTypes.string,
|
redirectUrl: PropTypes.string,
|
||||||
redirectToWelcomePage: PropTypes.bool,
|
redirectToProgressiveProfilingPage: PropTypes.bool,
|
||||||
|
optionalFields: PropTypes.shape({}),
|
||||||
|
redirectToRecommendationsPage: PropTypes.bool,
|
||||||
|
userId: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RedirectLogistration;
|
export default RedirectLogistration;
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Icon } from '@edx/paragon';
|
||||||
|
import { Login } from '@edx/paragon/icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||||
|
|
||||||
function SocialAuthProviders(props) {
|
const SocialAuthProviders = (props) => {
|
||||||
const { intl, referrer, socialAuthProviders } = props;
|
const { formatMessage } = useIntl();
|
||||||
|
const { referrer, socialAuthProviders } = props;
|
||||||
|
|
||||||
function handleSubmit(e) {
|
function handleSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -30,29 +32,30 @@ function SocialAuthProviders(props) {
|
|||||||
>
|
>
|
||||||
{provider.iconImage ? (
|
{provider.iconImage ? (
|
||||||
<div aria-hidden="true">
|
<div aria-hidden="true">
|
||||||
<img className="icon-image" src={provider.iconImage} alt={`icon ${provider.name}`} />
|
<img className="btn-tpa__image-icon" src={provider.iconImage} alt={`icon ${provider.name}`} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<>
|
<div className="btn-tpa__font-container" aria-hidden="true">
|
||||||
<div className="font-container" aria-hidden="true">
|
{SUPPORTED_ICON_CLASSES.includes(provider.iconClass) ? (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={['fab', provider.iconClass]} />)
|
||||||
icon={SUPPORTED_ICON_CLASSES.includes(provider.iconClass) ? ['fab', provider.iconClass] : faSignInAlt}
|
: (
|
||||||
/>
|
<Icon className="h-75" src={Login} />
|
||||||
</div>
|
)}
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
<span id="provider-name" className="notranslate mr-auto pl-2" aria-hidden="true">{provider.name}</span>
|
<span id="provider-name" className="notranslate mr-auto pl-2" aria-hidden="true">{provider.name}</span>
|
||||||
<span className="sr-only">
|
<span className="sr-only">
|
||||||
{referrer === LOGIN_PAGE
|
{referrer === LOGIN_PAGE
|
||||||
? intl.formatMessage(messages['sso.sign.in.with'], { providerName: provider.name })
|
? formatMessage(messages['sso.sign.in.with'], { providerName: provider.name })
|
||||||
: intl.formatMessage(messages['sso.create.account.using'], { providerName: provider.name })}
|
: formatMessage(messages['sso.create.account.using'], { providerName: provider.name })}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
return <>{socialAuth}</>;
|
return <>{socialAuth}</>;
|
||||||
}
|
};
|
||||||
|
|
||||||
SocialAuthProviders.defaultProps = {
|
SocialAuthProviders.defaultProps = {
|
||||||
referrer: LOGIN_PAGE,
|
referrer: LOGIN_PAGE,
|
||||||
@@ -60,7 +63,6 @@ SocialAuthProviders.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
SocialAuthProviders.propTypes = {
|
SocialAuthProviders.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
referrer: PropTypes.string,
|
referrer: PropTypes.string,
|
||||||
socialAuthProviders: PropTypes.arrayOf(PropTypes.shape({
|
socialAuthProviders: PropTypes.arrayOf(PropTypes.shape({
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
@@ -69,7 +71,8 @@ SocialAuthProviders.propTypes = {
|
|||||||
iconImage: PropTypes.string,
|
iconImage: PropTypes.string,
|
||||||
loginUrl: PropTypes.string,
|
loginUrl: PropTypes.string,
|
||||||
registerUrl: PropTypes.string,
|
registerUrl: PropTypes.string,
|
||||||
|
skipRegistrationForm: PropTypes.bool,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(SocialAuthProviders);
|
export default SocialAuthProviders;
|
||||||
|
|||||||
@@ -1,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 React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Alert } from '@edx/paragon';
|
import { Alert } from '@edx/paragon';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
||||||
|
|
||||||
const ThirdPartyAuthAlert = (props) => {
|
const ThirdPartyAuthAlert = (props) => {
|
||||||
const { currentProvider, intl, referrer } = props;
|
const { formatMessage } = useIntl();
|
||||||
|
const { currentProvider, referrer } = props;
|
||||||
const platformName = getConfig().SITE_NAME;
|
const platformName = getConfig().SITE_NAME;
|
||||||
let message;
|
let message;
|
||||||
|
|
||||||
if (referrer === LOGIN_PAGE) {
|
if (referrer === LOGIN_PAGE) {
|
||||||
message = intl.formatMessage(messages['login.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
message = formatMessage(messages['login.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
||||||
} else {
|
} else {
|
||||||
message = intl.formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
message = formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentProvider) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert id="tpa-alert" className={referrer === REGISTER_PAGE ? 'alert-success mt-n2' : 'alert-warning mt-n2'}>
|
<>
|
||||||
|
<Alert id="tpa-alert" className={referrer === REGISTER_PAGE ? 'alert-success mt-n2 mb-5' : 'alert-warning mt-n2 mb-5'}>
|
||||||
|
{referrer === REGISTER_PAGE ? (
|
||||||
|
<Alert.Heading>{formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
|
||||||
|
) : null}
|
||||||
|
<p>{ message }</p>
|
||||||
|
</Alert>
|
||||||
{referrer === REGISTER_PAGE ? (
|
{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}
|
) : null}
|
||||||
<p>{ message }</p>
|
</>
|
||||||
</Alert>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ThirdPartyAuthAlert.defaultProps = {
|
ThirdPartyAuthAlert.defaultProps = {
|
||||||
|
currentProvider: '',
|
||||||
referrer: LOGIN_PAGE,
|
referrer: LOGIN_PAGE,
|
||||||
};
|
};
|
||||||
|
|
||||||
ThirdPartyAuthAlert.propTypes = {
|
ThirdPartyAuthAlert.propTypes = {
|
||||||
currentProvider: PropTypes.string.isRequired,
|
currentProvider: PropTypes.string,
|
||||||
intl: intlShape.isRequired,
|
|
||||||
referrer: PropTypes.string,
|
referrer: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(ThirdPartyAuthAlert);
|
export default ThirdPartyAuthAlert;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Route } from 'react-router-dom';
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
|
import { Route } from 'react-router-dom';
|
||||||
|
|
||||||
import { DEFAULT_REDIRECT_URL } from '../data/constants';
|
import { DEFAULT_REDIRECT_URL } from '../data/constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,6 +23,9 @@ const UnAuthOnlyRoute = (props) => {
|
|||||||
|
|
||||||
if (isReady) {
|
if (isReady) {
|
||||||
if (authUser && authUser.username) {
|
if (authUser && authUser.username) {
|
||||||
|
if (true) { // check for query param
|
||||||
|
window.parent.postMessage({ authenticated: true }, 'https://discover.edx.org/social-reg');
|
||||||
|
}
|
||||||
global.location.href = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
|
global.location.href = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -28,7 +33,7 @@ const UnAuthOnlyRoute = (props) => {
|
|||||||
return <Route {...props} />;
|
return <Route {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <></>;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UnAuthOnlyRoute;
|
export default UnAuthOnlyRoute;
|
||||||
|
|||||||
56
src/common-components/Zendesk.jsx
Normal file
56
src/common-components/Zendesk.jsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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) },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Zendesk defer zendeskKey={getConfig().ZENDESK_KEY} {...setting} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ZendeskHelp;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AsyncActionType } from '../../data/utils';
|
import { AsyncActionType } from '../../data/utils';
|
||||||
|
|
||||||
export const THIRD_PARTY_AUTH_CONTEXT = new AsyncActionType('THIRD_PARTY_AUTH', 'GET_THIRD_PARTY_AUTH_CONTEXT');
|
export const THIRD_PARTY_AUTH_CONTEXT = new AsyncActionType('THIRD_PARTY_AUTH', 'GET_THIRD_PARTY_AUTH_CONTEXT');
|
||||||
|
export const THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG = 'THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG';
|
||||||
|
|
||||||
// Third party auth context
|
// Third party auth context
|
||||||
export const getThirdPartyAuthContext = (urlParams) => ({
|
export const getThirdPartyAuthContext = (urlParams) => ({
|
||||||
@@ -12,11 +13,15 @@ export const getThirdPartyAuthContextBegin = () => ({
|
|||||||
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
|
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getThirdPartyAuthContextSuccess = (thirdPartyAuthContext) => ({
|
export const getThirdPartyAuthContextSuccess = (fieldDescriptions, optionalFields, thirdPartyAuthContext) => ({
|
||||||
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
|
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
|
||||||
payload: { thirdPartyAuthContext },
|
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getThirdPartyAuthContextFailure = () => ({
|
export const getThirdPartyAuthContextFailure = () => ({
|
||||||
type: THIRD_PARTY_AUTH_CONTEXT.FAILURE,
|
type: THIRD_PARTY_AUTH_CONTEXT.FAILURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const clearThirdPartyAuthContextErrorMessage = () => ({
|
||||||
|
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,29 +1,51 @@
|
|||||||
import { THIRD_PARTY_AUTH_CONTEXT } from './actions';
|
import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from './actions';
|
||||||
|
import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants';
|
||||||
import { PENDING_STATE, COMPLETE_STATE } from '../../data/constants';
|
|
||||||
|
|
||||||
export const defaultState = {
|
export const defaultState = {
|
||||||
|
fieldDescriptions: {},
|
||||||
|
optionalFields: {},
|
||||||
thirdPartyAuthApiStatus: null,
|
thirdPartyAuthApiStatus: null,
|
||||||
|
thirdPartyAuthContext: {
|
||||||
|
currentProvider: null,
|
||||||
|
finishAuthUrl: null,
|
||||||
|
countryCode: null,
|
||||||
|
providers: [],
|
||||||
|
secondaryProviders: [],
|
||||||
|
pipelineUserDetails: null,
|
||||||
|
errorMessage: null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const reducer = (state = defaultState, action) => {
|
const reducer = (state = defaultState, action = {}) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case THIRD_PARTY_AUTH_CONTEXT.BEGIN:
|
case THIRD_PARTY_AUTH_CONTEXT.BEGIN:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||||
};
|
};
|
||||||
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS:
|
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS: {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
fieldDescriptions: action.payload.fieldDescriptions.fields,
|
||||||
|
optionalFields: action.payload.optionalFields,
|
||||||
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
|
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
|
||||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:
|
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||||
};
|
};
|
||||||
|
case THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||||
|
thirdPartyAuthContext: {
|
||||||
|
...state.thirdPartyAuthContext,
|
||||||
|
errorMessage: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
|
import { logError } from '@edx/frontend-platform/logging';
|
||||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||||
|
|
||||||
import { logError } from '@edx/frontend-platform/logging';
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
import {
|
import {
|
||||||
THIRD_PARTY_AUTH_CONTEXT,
|
|
||||||
getThirdPartyAuthContextBegin,
|
getThirdPartyAuthContextBegin,
|
||||||
getThirdPartyAuthContextSuccess,
|
|
||||||
getThirdPartyAuthContextFailure,
|
getThirdPartyAuthContextFailure,
|
||||||
|
getThirdPartyAuthContextSuccess,
|
||||||
|
THIRD_PARTY_AUTH_CONTEXT,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
|
|
||||||
// Services
|
|
||||||
import {
|
import {
|
||||||
getThirdPartyAuthContext,
|
getThirdPartyAuthContext,
|
||||||
} from './service';
|
} from './service';
|
||||||
|
import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions';
|
||||||
|
|
||||||
export function* fetchThirdPartyAuthContext(action) {
|
export function* fetchThirdPartyAuthContext(action) {
|
||||||
try {
|
try {
|
||||||
yield put(getThirdPartyAuthContextBegin());
|
yield put(getThirdPartyAuthContextBegin());
|
||||||
const { thirdPartyAuthContext } = yield call(getThirdPartyAuthContext, action.payload.urlParams);
|
const {
|
||||||
|
fieldDescriptions, optionalFields, thirdPartyAuthContext,
|
||||||
|
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
|
||||||
|
|
||||||
yield put(getThirdPartyAuthContextSuccess(
|
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
|
||||||
thirdPartyAuthContext,
|
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
|
||||||
));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
yield put(getThirdPartyAuthContextFailure());
|
yield put(getThirdPartyAuthContextFailure());
|
||||||
logError(e);
|
logError(e);
|
||||||
|
|||||||
@@ -8,3 +8,21 @@ export const thirdPartyAuthContextSelector = createSelector(
|
|||||||
commonComponentsSelector,
|
commonComponentsSelector,
|
||||||
commonComponents => commonComponents.thirdPartyAuthContext,
|
commonComponents => commonComponents.thirdPartyAuthContext,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const fieldDescriptionSelector = createSelector(
|
||||||
|
commonComponentsSelector,
|
||||||
|
commonComponents => commonComponents.fieldDescriptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const optionalFieldsSelector = createSelector(
|
||||||
|
commonComponentsSelector,
|
||||||
|
commonComponents => commonComponents.optionalFields,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const tpaProvidersSelector = createSelector(
|
||||||
|
commonComponentsSelector,
|
||||||
|
commonComponents => ({
|
||||||
|
providers: commonComponents.thirdPartyAuthContext.providers,
|
||||||
|
secondaryProviders: commonComponents.thirdPartyAuthContext.secondaryProviders,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { camelCaseObject, convertKeyNames, getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
@@ -18,6 +18,8 @@ export async function getThirdPartyAuthContext(urlParams) {
|
|||||||
throw (e);
|
throw (e);
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
thirdPartyAuthContext: camelCaseObject(convertKeyNames(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 occured',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = {
|
||||||
|
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
reducer(state, action),
|
||||||
|
).toEqual(
|
||||||
|
{
|
||||||
|
...state,
|
||||||
|
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||||
|
thirdPartyAuthContext: {
|
||||||
|
...state.thirdPartyAuthContext,
|
||||||
|
errorMessage: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { runSaga } from 'redux-saga';
|
import { runSaga } from 'redux-saga';
|
||||||
|
|
||||||
|
import { setCountryFromThirdPartyAuthContext } from '../../../register/data/actions';
|
||||||
|
import initializeMockLogging from '../../../setupTest';
|
||||||
import * as actions from '../actions';
|
import * as actions from '../actions';
|
||||||
import { fetchThirdPartyAuthContext } from '../sagas';
|
import { fetchThirdPartyAuthContext } from '../sagas';
|
||||||
import * as api from '../service';
|
import * as api from '../service';
|
||||||
import initializeMockLogging from '../../../setupTest';
|
|
||||||
|
|
||||||
const { loggingService } = initializeMockLogging();
|
const { loggingService } = initializeMockLogging();
|
||||||
|
|
||||||
@@ -26,7 +27,11 @@ describe('fetchThirdPartyAuthContext', () => {
|
|||||||
|
|
||||||
it('should call service and dispatch success action', async () => {
|
it('should call service and dispatch success action', async () => {
|
||||||
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
|
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
|
||||||
.mockImplementation(() => Promise.resolve({ thirdPartyAuthContext: data }));
|
.mockImplementation(() => Promise.resolve({
|
||||||
|
thirdPartyAuthContext: data,
|
||||||
|
fieldDescriptions: {},
|
||||||
|
optionalFields: {},
|
||||||
|
}));
|
||||||
|
|
||||||
const dispatched = [];
|
const dispatched = [];
|
||||||
await runSaga(
|
await runSaga(
|
||||||
@@ -38,7 +43,8 @@ describe('fetchThirdPartyAuthContext', () => {
|
|||||||
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
|
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
|
||||||
expect(dispatched).toEqual([
|
expect(dispatched).toEqual([
|
||||||
actions.getThirdPartyAuthContextBegin(),
|
actions.getThirdPartyAuthContextBegin(),
|
||||||
actions.getThirdPartyAuthContextSuccess(data),
|
setCountryFromThirdPartyAuthContext(),
|
||||||
|
actions.getThirdPartyAuthContextSuccess({}, {}, data),
|
||||||
]);
|
]);
|
||||||
getThirdPartyAuthContext.mockClear();
|
getThirdPartyAuthContext.mockClear();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ export { default as saga } from './data/sagas';
|
|||||||
export { storeName } from './data/selectors';
|
export { storeName } from './data/selectors';
|
||||||
export { default as FormGroup } from './FormGroup';
|
export { default as FormGroup } from './FormGroup';
|
||||||
export { default as PasswordField } from './PasswordField';
|
export { default as PasswordField } from './PasswordField';
|
||||||
export { default as Logistration } from './Logistration';
|
export { default as Zendesk } from './Zendesk';
|
||||||
|
|||||||
@@ -1,29 +1,13 @@
|
|||||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
// institution login strings
|
||||||
'institution.login.page.sub.heading': {
|
'institution.login.page.sub.heading': {
|
||||||
id: 'institution.login.page.sub.heading',
|
id: 'institution.login.page.sub.heading',
|
||||||
defaultMessage: 'Choose your institution from the list below',
|
defaultMessage: 'Choose your institution from the list below',
|
||||||
description: 'Heading of the institutions list',
|
description: 'Heading of the institutions list',
|
||||||
},
|
},
|
||||||
// Confirmation Alert Message
|
// logistration strings
|
||||||
'forgot.password.confirmation.title': {
|
|
||||||
id: 'forgot.password.confirmation.title',
|
|
||||||
defaultMessage: 'Check your email',
|
|
||||||
description: 'Forgot password confirmation message title',
|
|
||||||
},
|
|
||||||
'forgot.password.confirmation.support.link': {
|
|
||||||
id: 'forgot.password.confirmation.support.link',
|
|
||||||
defaultMessage: 'contact technical support',
|
|
||||||
description: 'Technical support link text',
|
|
||||||
},
|
|
||||||
'forgot.password.confirmation.info': {
|
|
||||||
id: 'forgot.password.confirmation.info',
|
|
||||||
defaultMessage: 'If you do not receive a password reset message after 1 minute, verify that you entered the correct '
|
|
||||||
+ 'email address, or check your spam folder.',
|
|
||||||
description: 'Part of message that appears after user requests password change',
|
|
||||||
},
|
|
||||||
// Logistration strinsg
|
|
||||||
'logistration.sign.in': {
|
'logistration.sign.in': {
|
||||||
id: 'logistration.sign.in',
|
id: 'logistration.sign.in',
|
||||||
defaultMessage: 'Sign in',
|
defaultMessage: 'Sign in',
|
||||||
@@ -34,32 +18,22 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Register',
|
defaultMessage: 'Register',
|
||||||
description: 'Text that appears on the tab to switch between login and register',
|
description: 'Text that appears on the tab to switch between login and register',
|
||||||
},
|
},
|
||||||
'internal.server.error.message': {
|
|
||||||
id: 'internal.server.error.message',
|
|
||||||
defaultMessage: 'An error has occurred. Try refreshing the page, or check your internet connection.',
|
|
||||||
description: 'Error message that appears when server responds with 500 error code',
|
|
||||||
},
|
|
||||||
'server.ratelimit.error.message': {
|
|
||||||
id: 'server.ratelimit.error.message',
|
|
||||||
defaultMessage: 'An error has occurred because of too many requests. Please try again after some time.',
|
|
||||||
description: 'Error message that appears when server responds with 429 error code',
|
|
||||||
},
|
|
||||||
// enterprise sso strings
|
// enterprise sso strings
|
||||||
'enterprisetpa.title.heading': {
|
'enterprisetpa.title.heading': {
|
||||||
id: 'enterprisetpa.title.heading',
|
id: 'enterprisetpa.title.heading',
|
||||||
defaultMessage: 'Would you like to sign in using your {providerName} credentials?',
|
defaultMessage: 'Would you like to sign in using your {providerName} credentials?',
|
||||||
description: 'Header text used in enterprise third party authentication',
|
description: 'Header text used in enterprise third party authentication',
|
||||||
},
|
},
|
||||||
'enterprisetpa.sso.button.title': {
|
|
||||||
id: 'enterprisetpa.sso.button.title',
|
|
||||||
defaultMessage: 'Sign in using {providerName}',
|
|
||||||
description: 'Text for third party auth provider buttons',
|
|
||||||
},
|
|
||||||
'enterprisetpa.login.button.text': {
|
'enterprisetpa.login.button.text': {
|
||||||
id: 'enterprisetpa.login.button.text',
|
id: 'enterprisetpa.login.button.text',
|
||||||
defaultMessage: 'Show me other ways to sign in or register',
|
defaultMessage: 'Show me other ways to sign in or register',
|
||||||
description: 'Button text for login',
|
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
|
// social auth providers
|
||||||
'sso.sign.in.with': {
|
'sso.sign.in.with': {
|
||||||
id: 'sso.sign.in.with',
|
id: 'sso.sign.in.with',
|
||||||
@@ -84,17 +58,17 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
'one.letter': {
|
'one.letter': {
|
||||||
id: 'one.letter',
|
id: 'one.letter',
|
||||||
defaultMessage: '1 Letter',
|
defaultMessage: '1 letter',
|
||||||
description: 'password requirement to have 1 letter',
|
description: 'password requirement to have 1 letter',
|
||||||
},
|
},
|
||||||
'one.number': {
|
'one.number': {
|
||||||
id: 'one.number',
|
id: 'one.number',
|
||||||
defaultMessage: '1 Number',
|
defaultMessage: '1 number',
|
||||||
description: 'password requirement to have 1 number',
|
description: 'password requirement to have 1 number',
|
||||||
},
|
},
|
||||||
'eight.characters': {
|
'eight.characters': {
|
||||||
id: 'eight.characters',
|
id: 'eight.characters',
|
||||||
defaultMessage: '8 Characters',
|
defaultMessage: '8 characters',
|
||||||
description: 'password requirement to have a minimum of 8 characters',
|
description: 'password requirement to have a minimum of 8 characters',
|
||||||
},
|
},
|
||||||
'password.sr.only.helping.text': {
|
'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 '
|
description: 'Message that appears on register page if user has successfully authenticated with TPA '
|
||||||
+ 'but no associated platform account exists',
|
+ 'but no associated platform account exists',
|
||||||
},
|
},
|
||||||
|
'registration.using.tpa.form.heading': {
|
||||||
|
id: 'registration.using.tpa.form.heading',
|
||||||
|
defaultMessage: 'Finish creating your account',
|
||||||
|
description: 'Heading that appears above form when user is trying to create account using social auth',
|
||||||
|
},
|
||||||
|
supportTitle: {
|
||||||
|
id: 'zendesk.supportTitle',
|
||||||
|
description: 'Title for the support button',
|
||||||
|
defaultMessage: 'edX Support',
|
||||||
|
},
|
||||||
|
selectTicketForm: {
|
||||||
|
id: 'zendesk.selectTicketForm',
|
||||||
|
description: 'Select ticket form',
|
||||||
|
defaultMessage: 'Please choose your request type:',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
|
||||||
import { act } from 'react-dom/test-utils';
|
|
||||||
|
|
||||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
import FormGroup from '../FormGroup';
|
import FormGroup from '../FormGroup';
|
||||||
import PasswordField from '../PasswordField';
|
import PasswordField from '../PasswordField';
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React from 'react';
|
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 registerIcons from '../RegisterFaIcons';
|
||||||
|
import SocialAuthProviders from '../SocialAuthProviders';
|
||||||
|
|
||||||
registerIcons();
|
registerIcons();
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React from 'react';
|
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 { REGISTER_PAGE } from '../../data/constants';
|
||||||
|
import ThirdPartyAuthAlert from '../ThirdPartyAuthAlert';
|
||||||
|
|
||||||
describe('ThirdPartyAuthAlert', () => {
|
describe('ThirdPartyAuthAlert', () => {
|
||||||
let props = {};
|
let props = {};
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
|
/* eslint-disable import/no-import-module-exports */
|
||||||
|
/* eslint-disable react/function-component-definition */
|
||||||
import React from 'react';
|
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 { UnAuthOnlyRoute } from '..';
|
||||||
import { LOGIN_PAGE } from '../../data/constants';
|
import { LOGIN_PAGE } from '../../data/constants';
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/auth');
|
import { MemoryRouter, BrowserRouter as Router, Switch } from 'react-router-dom';
|
||||||
|
|
||||||
|
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||||
|
getAuthenticatedUser: jest.fn(),
|
||||||
|
fetchAuthenticatedUser: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
const RRD = require('react-router-dom');
|
const RRD = require('react-router-dom');
|
||||||
// Just render plain div with its children
|
// Just render plain div with its children
|
||||||
@@ -36,25 +43,30 @@ describe('UnAuthOnlyRoute', () => {
|
|||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have called with forceRefresh true', () => {
|
it('should have called with forceRefresh true', async () => {
|
||||||
const user = {
|
const user = {
|
||||||
username: 'gonzo',
|
username: 'gonzo',
|
||||||
other: 'data',
|
other: 'data',
|
||||||
};
|
};
|
||||||
auth.getAuthenticatedUser = jest.fn(() => user);
|
|
||||||
auth.fetchAuthenticatedUser = jest.fn(() => ({ then: () => auth.getAuthenticatedUser() }));
|
|
||||||
|
|
||||||
mount(routerWrapper());
|
getAuthenticatedUser.mockReturnValue(user);
|
||||||
|
fetchAuthenticatedUser.mockReturnValueOnce(Promise.resolve(user));
|
||||||
|
|
||||||
expect(auth.fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: true });
|
await act(async () => {
|
||||||
|
await mount(routerWrapper());
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have called with forceRefresh false', () => {
|
it('should have called with forceRefresh false', async () => {
|
||||||
auth.getAuthenticatedUser = jest.fn(() => null);
|
getAuthenticatedUser.mockReturnValue(null);
|
||||||
auth.fetchAuthenticatedUser = jest.fn(() => ({ then: () => auth.getAuthenticatedUser() }));
|
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
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="font-container"
|
className="btn-tpa__font-container"
|
||||||
>
|
>
|
||||||
<svg
|
<span
|
||||||
aria-hidden="true"
|
className="pgn__icon h-75"
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
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"
|
aria-hidden={true}
|
||||||
fill="currentColor"
|
fill="none"
|
||||||
style={Object {}}
|
focusable={false}
|
||||||
/>
|
height={24}
|
||||||
</svg>
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width={24}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M11 7L9.6 8.4l2.6 2.6H2v2h10.2l-2.6 2.6L11 17l5-5-5-5zm9 12h-8v2h10V3H12v2h8v14z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -55,11 +57,11 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="font-container"
|
className="btn-tpa__font-container"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="svg-inline--fa fa-google fa-w-16 "
|
className="svg-inline--fa fa-google "
|
||||||
data-icon="google"
|
data-icon="google"
|
||||||
data-prefix="fab"
|
data-prefix="fab"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
@@ -104,7 +106,7 @@ Array [
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="icon Apple"
|
alt="icon Apple"
|
||||||
className="icon-image"
|
className="btn-tpa__image-icon"
|
||||||
src="https://edx.devstack.lms/logo.png"
|
src="https://edx.devstack.lms/logo.png"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,7 +135,7 @@ Array [
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="icon Facebook"
|
alt="icon Facebook"
|
||||||
className="icon-image"
|
className="btn-tpa__image-icon"
|
||||||
src="https://edx.devstack.lms/facebook-logo.png"
|
src="https://edx.devstack.lms/facebook-logo.png"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
exports[`ThirdPartyAuthAlert should match login page third party auth alert message snapshot 1`] = `
|
exports[`ThirdPartyAuthAlert should match login page third party auth alert message snapshot 1`] = `
|
||||||
<div
|
<div
|
||||||
className="fade alert-content alert-warning mt-n2 alert show"
|
className="fade alert-content alert-warning mt-n2 mb-5 alert show"
|
||||||
id="tpa-alert"
|
id="tpa-alert"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
@@ -21,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`] = `
|
exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = `
|
||||||
<div
|
Array [
|
||||||
className="fade alert-content alert-success mt-n2 alert show"
|
|
||||||
id="tpa-alert"
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="pgn__alert-message-wrapper"
|
className="fade alert-content alert-success mt-n2 mb-5 alert show"
|
||||||
|
id="tpa-alert"
|
||||||
|
role="alert"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="alert-message-content"
|
className="pgn__alert-message-wrapper"
|
||||||
>
|
>
|
||||||
<div
|
<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>
|
</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>,
|
||||||
</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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
`;
|
||||||
28
src/config/index.js
Normal file
28
src/config/index.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
const configuration = {
|
||||||
|
// Cookies related configs
|
||||||
|
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN,
|
||||||
|
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME || null,
|
||||||
|
USER_SURVEY_COOKIE_NAME: process.env.USER_SURVEY_COOKIE_NAME || null,
|
||||||
|
// Features
|
||||||
|
DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '',
|
||||||
|
ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,
|
||||||
|
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: process.env.ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN || false,
|
||||||
|
ENABLE_PERSONALIZED_RECOMMENDATIONS: process.env.ENABLE_PERSONALIZED_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,
|
||||||
|
PRIVACY_POLICY: process.env.PRIVACY_POLICY || null,
|
||||||
|
TOS_AND_HONOR_CODE: process.env.TOS_AND_HONOR_CODE || null,
|
||||||
|
TOS_LINK: process.env.TOS_LINK || null,
|
||||||
|
// Miscellaneous
|
||||||
|
GENERAL_RECOMMENDATIONS: process.env.GENERAL_RECOMMENDATIONS || '[]',
|
||||||
|
INFO_EMAIL: process.env.INFO_EMAIL || '',
|
||||||
|
ZENDESK_KEY: process.env.ZENDESK_KEY,
|
||||||
|
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default configuration;
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { applyMiddleware, createStore, compose } from 'redux';
|
|
||||||
import thunkMiddleware from 'redux-thunk';
|
|
||||||
import { composeWithDevTools } from '@redux-devtools/extension';
|
import { composeWithDevTools } from '@redux-devtools/extension';
|
||||||
|
import { applyMiddleware, compose, createStore } from 'redux';
|
||||||
import { createLogger } from 'redux-logger';
|
import { createLogger } from 'redux-logger';
|
||||||
import createSagaMiddleware from 'redux-saga';
|
import createSagaMiddleware from 'redux-saga';
|
||||||
|
import thunkMiddleware from 'redux-thunk';
|
||||||
|
|
||||||
import createRootReducer from './reducers';
|
import createRootReducer from './reducers';
|
||||||
import rootSaga from './sagas';
|
import rootSaga from './sagas';
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
export const LOGIN_PAGE = '/login';
|
export const LOGIN_PAGE = '/login';
|
||||||
export const REGISTER_PAGE = '/register';
|
export const REGISTER_PAGE = '/register';
|
||||||
export const RESET_PAGE = '/reset';
|
export const RESET_PAGE = '/reset';
|
||||||
export const WELCOME_PAGE = '/welcome';
|
export const AUTHN_PROGRESSIVE_PROFILING = '/welcome';
|
||||||
export const DEFAULT_REDIRECT_URL = '/dashboard';
|
export const DEFAULT_REDIRECT_URL = '/dashboard';
|
||||||
|
export const RECOMMENDATIONS = '/recommendations';
|
||||||
export const PASSWORD_RESET_CONFIRM = '/password_reset_confirm/:token/';
|
export const PASSWORD_RESET_CONFIRM = '/password_reset_confirm/:token/';
|
||||||
export const PAGE_NOT_FOUND = '/notfound';
|
export const PAGE_NOT_FOUND = '/notfound';
|
||||||
export const ENTERPRISE_LOGIN_URL = '/enterprise/login';
|
export const ENTERPRISE_LOGIN_URL = '/enterprise/login';
|
||||||
@@ -12,14 +13,16 @@ export const ENTERPRISE_LOGIN_URL = '/enterprise/login';
|
|||||||
export const SUPPORTED_ICON_CLASSES = ['apple', 'facebook', 'google', 'microsoft'];
|
export const SUPPORTED_ICON_CLASSES = ['apple', 'facebook', 'google', 'microsoft'];
|
||||||
|
|
||||||
// Error Codes
|
// Error Codes
|
||||||
|
export const FORM_SUBMISSION_ERROR = 'form-submission-error';
|
||||||
export const INTERNAL_SERVER_ERROR = 'internal-server-error';
|
export const INTERNAL_SERVER_ERROR = 'internal-server-error';
|
||||||
export const API_RATELIMIT_ERROR = 'api-ratelimit-error';
|
export const API_RATELIMIT_ERROR = 'api-ratelimit-error';
|
||||||
|
|
||||||
// States
|
// Common States
|
||||||
export const DEFAULT_STATE = 'default';
|
export const DEFAULT_STATE = 'default';
|
||||||
export const PENDING_STATE = 'pending';
|
export const PENDING_STATE = 'pending';
|
||||||
export const COMPLETE_STATE = 'complete';
|
export const COMPLETE_STATE = 'complete';
|
||||||
export const FAILURE_STATE = 'failure';
|
export const FAILURE_STATE = 'failure';
|
||||||
|
export const FORBIDDEN_STATE = 'forbidden';
|
||||||
|
|
||||||
// Regex
|
// Regex
|
||||||
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'
|
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'
|
||||||
@@ -28,8 +31,8 @@ export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+
|
|||||||
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$';
|
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$';
|
||||||
export const LETTER_REGEX = /[a-zA-Z]/;
|
export const LETTER_REGEX = /[a-zA-Z]/;
|
||||||
export const NUMBER_REGEX = /\d/;
|
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
|
export const INVALID_NAME_REGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; // eslint-disable-line no-useless-escape
|
||||||
|
|
||||||
// Query string parameters that can be passed to LMS to manage
|
// Query string parameters that can be passed to LMS to manage
|
||||||
// things like auto-enrollment upon login and registration.
|
// things like auto-enrollment upon login and registration.
|
||||||
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'save_for_later', 'register_for_free'];
|
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'save_for_later', 'register_for_free', 'track', 'is_account_recovery'];
|
||||||
|
|||||||
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();
|
||||||
@@ -1,13 +1,5 @@
|
|||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
|
|
||||||
import {
|
|
||||||
reducer as loginReducer,
|
|
||||||
storeName as loginStoreName,
|
|
||||||
} from '../login';
|
|
||||||
import {
|
|
||||||
reducer as registerReducer,
|
|
||||||
storeName as registerStoreName,
|
|
||||||
} from '../register';
|
|
||||||
import {
|
import {
|
||||||
reducer as commonComponentsReducer,
|
reducer as commonComponentsReducer,
|
||||||
storeName as commonComponentsStoreName,
|
storeName as commonComponentsStoreName,
|
||||||
@@ -16,22 +8,29 @@ import {
|
|||||||
reducer as forgotPasswordReducer,
|
reducer as forgotPasswordReducer,
|
||||||
storeName as forgotPasswordStoreName,
|
storeName as forgotPasswordStoreName,
|
||||||
} from '../forgot-password';
|
} from '../forgot-password';
|
||||||
|
import {
|
||||||
|
reducer as loginReducer,
|
||||||
|
storeName as loginStoreName,
|
||||||
|
} from '../login';
|
||||||
|
import {
|
||||||
|
reducer as authnProgressiveProfilingReducers,
|
||||||
|
storeName as authnProgressiveProfilingStoreName,
|
||||||
|
} from '../progressive-profiling';
|
||||||
|
import {
|
||||||
|
reducer as registerReducer,
|
||||||
|
storeName as registerStoreName,
|
||||||
|
} from '../register';
|
||||||
import {
|
import {
|
||||||
reducer as resetPasswordReducer,
|
reducer as resetPasswordReducer,
|
||||||
storeName as resetPasswordStoreName,
|
storeName as resetPasswordStoreName,
|
||||||
} from '../reset-password';
|
} from '../reset-password';
|
||||||
|
|
||||||
import {
|
|
||||||
reducer as welcomePageReducers,
|
|
||||||
storeName as welcomePageStoreName,
|
|
||||||
} from '../welcome';
|
|
||||||
|
|
||||||
const createRootReducer = () => combineReducers({
|
const createRootReducer = () => combineReducers({
|
||||||
[loginStoreName]: loginReducer,
|
[loginStoreName]: loginReducer,
|
||||||
[registerStoreName]: registerReducer,
|
[registerStoreName]: registerReducer,
|
||||||
[commonComponentsStoreName]: commonComponentsReducer,
|
[commonComponentsStoreName]: commonComponentsReducer,
|
||||||
[forgotPasswordStoreName]: forgotPasswordReducer,
|
[forgotPasswordStoreName]: forgotPasswordReducer,
|
||||||
[resetPasswordStoreName]: resetPasswordReducer,
|
[resetPasswordStoreName]: resetPasswordReducer,
|
||||||
[welcomePageStoreName]: welcomePageReducers,
|
[authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers,
|
||||||
});
|
});
|
||||||
export default createRootReducer;
|
export default createRootReducer;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { all } from 'redux-saga/effects';
|
import { all } from 'redux-saga/effects';
|
||||||
|
|
||||||
import { saga as registrationSaga } from '../register';
|
|
||||||
import { saga as loginSaga } from '../login';
|
|
||||||
import { saga as commonComponentsSaga } from '../common-components';
|
import { saga as commonComponentsSaga } from '../common-components';
|
||||||
import { saga as forgotPasswordSaga } from '../forgot-password';
|
import { saga as forgotPasswordSaga } from '../forgot-password';
|
||||||
|
import { saga as loginSaga } from '../login';
|
||||||
|
import { saga as authnProgressiveProfilingSaga } from '../progressive-profiling';
|
||||||
|
import { saga as registrationSaga } from '../register';
|
||||||
import { saga as resetPasswordSaga } from '../reset-password';
|
import { saga as resetPasswordSaga } from '../reset-password';
|
||||||
import { saga as welcomePageSaga } from '../welcome';
|
|
||||||
|
|
||||||
export default function* rootSaga() {
|
export default function* rootSaga() {
|
||||||
yield all([
|
yield all([
|
||||||
@@ -14,6 +14,6 @@ export default function* rootSaga() {
|
|||||||
commonComponentsSaga(),
|
commonComponentsSaga(),
|
||||||
forgotPasswordSaga(),
|
forgotPasswordSaga(),
|
||||||
resetPasswordSaga(),
|
resetPasswordSaga(),
|
||||||
welcomePageSaga(),
|
authnProgressiveProfilingSaga(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import Cookies from 'universal-cookie';
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import Cookies from 'universal-cookie';
|
||||||
|
|
||||||
export function setCookie(cookieName, cookieValue, cookieExpiry) {
|
export function setCookie(cookieName, cookieValue, cookieExpiry) {
|
||||||
const cookies = new Cookies();
|
const cookies = new Cookies();
|
||||||
const options = { domain: getConfig().COOKIE_DOMAIN, path: '/' };
|
const options = { domain: getConfig().SESSION_COOKIE_DOMAIN, path: '/' };
|
||||||
if (cookieExpiry) {
|
if (cookieExpiry) {
|
||||||
options.expires = cookieExpiry;
|
options.expires = cookieExpiry;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
// Utility functions
|
// Utility functions
|
||||||
|
|
||||||
import * as QueryString from 'query-string';
|
import * as QueryString from 'query-string';
|
||||||
import { AUTH_PARAMS } from '../constants';
|
|
||||||
|
|
||||||
export default function processLink(link) {
|
import { AUTH_PARAMS } from '../constants';
|
||||||
let matches;
|
|
||||||
link.replace(/(.*?)<a href=["']([^"']*).*?>([^<]+)<\/a>(.*)/g, function () { // eslint-disable-line func-names
|
|
||||||
matches = Array.prototype.slice.call(arguments, 1, 5); // eslint-disable-line prefer-rest-params
|
|
||||||
});
|
|
||||||
return matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getTpaProvider = (tpaHintProvider, primaryProviders, secondaryProviders) => {
|
export const getTpaProvider = (tpaHintProvider, primaryProviders, secondaryProviders) => {
|
||||||
let tpaProvider = null;
|
let tpaProvider = null;
|
||||||
@@ -57,8 +49,8 @@ export const updatePathWithQueryParams = (path) => {
|
|||||||
return `${path}${queryParams}`;
|
return `${path}${queryParams}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllPossibleQueryParam = () => {
|
export const getAllPossibleQueryParams = (locationURl = null) => {
|
||||||
const urlParams = QueryString.parse(window.location.search);
|
const urlParams = locationURl ? QueryString.parseUrl(locationURl).query : QueryString.parse(window.location.search);
|
||||||
const params = {};
|
const params = {};
|
||||||
Object.entries(urlParams).forEach(([key, value]) => {
|
Object.entries(urlParams).forEach(([key, value]) => {
|
||||||
if (AUTH_PARAMS.indexOf(key) > -1) {
|
if (AUTH_PARAMS.indexOf(key) > -1) {
|
||||||
|
|||||||
@@ -1,18 +1,5 @@
|
|||||||
|
import { updatePathWithQueryParams } from './dataUtils';
|
||||||
import { LOGIN_PAGE } from '../constants';
|
import { LOGIN_PAGE } from '../constants';
|
||||||
import processLink, { updatePathWithQueryParams } from './dataUtils';
|
|
||||||
|
|
||||||
describe('processLink', () => {
|
|
||||||
it('should use the provided processLink function to', () => {
|
|
||||||
const expectedHref = 'http://test.server.com/';
|
|
||||||
const expectedText = 'test link';
|
|
||||||
const link = `<a href="${expectedHref}">${expectedText}</a>`;
|
|
||||||
|
|
||||||
const matches = processLink(link);
|
|
||||||
|
|
||||||
expect(matches[1]).toEqual(expectedHref);
|
|
||||||
expect(matches[2]).toEqual(expectedText);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updatePathWithQueryParams', () => {
|
describe('updatePathWithQueryParams', () => {
|
||||||
it('should append query params into the path', () => {
|
it('should append query params into the path', () => {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
export {
|
export {
|
||||||
default,
|
|
||||||
getTpaProvider,
|
getTpaProvider,
|
||||||
getTpaHint,
|
getTpaHint,
|
||||||
updatePathWithQueryParams,
|
updatePathWithQueryParams,
|
||||||
getAllPossibleQueryParam,
|
getAllPossibleQueryParams,
|
||||||
getActivationStatus,
|
getActivationStatus,
|
||||||
windowScrollTo,
|
windowScrollTo,
|
||||||
} from './dataUtils';
|
} from './dataUtils';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { breakpoints } from '@edx/paragon';
|
import { breakpoints } from '@edx/paragon';
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ const useMobileResponsive = (breakpoint) => {
|
|||||||
window.addEventListener('resize', checkForMobile);
|
window.addEventListener('resize', checkForMobile);
|
||||||
// return this function here to clean up the event listener
|
// return this function here to clean up the event listener
|
||||||
return () => window.removeEventListener('resize', checkForMobile);
|
return () => window.removeEventListener('resize', checkForMobile);
|
||||||
}, []);
|
});
|
||||||
return isMobileWindow;
|
return isMobileWindow;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Form, Icon } from '@edx/paragon';
|
import { Form, Icon } from '@edx/paragon';
|
||||||
import { ExpandMore } from '@edx/paragon/icons';
|
import { ExpandMore } from '@edx/paragon/icons';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const FormFieldRenderer = (props) => {
|
const FormFieldRenderer = (props) => {
|
||||||
let formField = null;
|
let formField = null;
|
||||||
const { fieldData, onChangeHandler, value } = props;
|
const {
|
||||||
|
className, errorMessage, fieldData, onChangeHandler, isRequired, value,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const handleFocus = (e) => {
|
||||||
|
if (props.handleFocus) { props.handleFocus(e); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnBlur = (e) => {
|
||||||
|
if (props.handleBlur) { props.handleBlur(e); }
|
||||||
|
};
|
||||||
|
|
||||||
switch (fieldData.type) {
|
switch (fieldData.type) {
|
||||||
case 'select': {
|
case 'select': {
|
||||||
@@ -14,63 +24,99 @@ const FormFieldRenderer = (props) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
formField = (
|
formField = (
|
||||||
<Form.Group controlId={fieldData.name} className="mb-3">
|
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
className={className}
|
||||||
as="select"
|
as="select"
|
||||||
name={fieldData.name}
|
name={fieldData.name}
|
||||||
value={value}
|
value={value}
|
||||||
|
aria-invalid={isRequired && Boolean(errorMessage)}
|
||||||
onChange={(e) => onChangeHandler(e)}
|
onChange={(e) => onChangeHandler(e)}
|
||||||
trailingElement={<Icon src={ExpandMore} />}
|
trailingElement={<Icon src={ExpandMore} />}
|
||||||
floatingLabel={fieldData.label}
|
floatingLabel={fieldData.label}
|
||||||
|
onBlur={handleOnBlur}
|
||||||
|
onFocus={handleFocus}
|
||||||
>
|
>
|
||||||
<option key="default" value="">{fieldData.label}</option>
|
<option key="default" value="">{fieldData.label}</option>
|
||||||
{fieldData.options.map(option => (
|
{fieldData.options.map(option => (
|
||||||
<option className="data-hj-suppress" key={option[0]} value={option[0]}>{option[1]}</option>
|
<option className="data-hj-suppress" key={option[0]} value={option[0]}>{option[1]}</option>
|
||||||
))}
|
))}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
|
{isRequired && errorMessage && (
|
||||||
|
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
|
||||||
|
{errorMessage}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'textarea': {
|
case 'textarea': {
|
||||||
formField = (
|
formField = (
|
||||||
<Form.Group controlId={fieldData.name} className="mb-3">
|
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
className={className}
|
||||||
as="textarea"
|
as="textarea"
|
||||||
name={fieldData.name}
|
name={fieldData.name}
|
||||||
value={value}
|
value={value}
|
||||||
|
aria-invalid={isRequired && Boolean(errorMessage)}
|
||||||
onChange={(e) => onChangeHandler(e)}
|
onChange={(e) => onChangeHandler(e)}
|
||||||
floatingLabel={fieldData.label}
|
floatingLabel={fieldData.label}
|
||||||
|
onBlur={handleOnBlur}
|
||||||
|
onFocus={handleFocus}
|
||||||
/>
|
/>
|
||||||
|
{isRequired && errorMessage && (
|
||||||
|
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
|
||||||
|
{errorMessage}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'text': {
|
case 'text': {
|
||||||
formField = (
|
formField = (
|
||||||
<Form.Group controlId={fieldData.name} className="mb-3">
|
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
className={className}
|
||||||
name={fieldData.name}
|
name={fieldData.name}
|
||||||
value={value}
|
value={value}
|
||||||
|
aria-invalid={isRequired && Boolean(errorMessage)}
|
||||||
onChange={(e) => onChangeHandler(e)}
|
onChange={(e) => onChangeHandler(e)}
|
||||||
floatingLabel={fieldData.label}
|
floatingLabel={fieldData.label}
|
||||||
|
onBlur={handleOnBlur}
|
||||||
|
onFocus={handleFocus}
|
||||||
/>
|
/>
|
||||||
|
{isRequired && errorMessage && (
|
||||||
|
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
|
||||||
|
{errorMessage}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'checkbox': {
|
case 'checkbox': {
|
||||||
formField = (
|
formField = (
|
||||||
<Form.Group className="mb-3">
|
<Form.Group isInvalid={!!(isRequired && errorMessage)}>
|
||||||
<Form.Checkbox
|
<Form.Checkbox
|
||||||
|
className={className}
|
||||||
id={fieldData.name}
|
id={fieldData.name}
|
||||||
checked={!!value}
|
checked={!!value}
|
||||||
name={fieldData.name}
|
name={fieldData.name}
|
||||||
value={value}
|
value={value}
|
||||||
|
aria-invalid={isRequired && Boolean(errorMessage)}
|
||||||
onChange={(e) => onChangeHandler(e)}
|
onChange={(e) => onChangeHandler(e)}
|
||||||
|
onBlur={handleOnBlur}
|
||||||
|
onFocus={handleFocus}
|
||||||
>
|
>
|
||||||
{fieldData.label}
|
{fieldData.label}
|
||||||
</Form.Checkbox>
|
</Form.Checkbox>
|
||||||
|
{isRequired && errorMessage && (
|
||||||
|
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
|
||||||
|
{errorMessage}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -82,17 +128,31 @@ const FormFieldRenderer = (props) => {
|
|||||||
return formField;
|
return formField;
|
||||||
};
|
};
|
||||||
FormFieldRenderer.defaultProps = {
|
FormFieldRenderer.defaultProps = {
|
||||||
|
className: '',
|
||||||
value: '',
|
value: '',
|
||||||
|
handleBlur: null,
|
||||||
|
handleFocus: null,
|
||||||
|
errorMessage: '',
|
||||||
|
isRequired: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
FormFieldRenderer.propTypes = {
|
FormFieldRenderer.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
fieldData: PropTypes.shape({
|
fieldData: PropTypes.shape({
|
||||||
type: PropTypes.string,
|
type: PropTypes.string,
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
|
options: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
onChangeHandler: PropTypes.func.isRequired,
|
onChangeHandler: PropTypes.func.isRequired,
|
||||||
value: PropTypes.string,
|
handleBlur: PropTypes.func,
|
||||||
|
handleFocus: PropTypes.func,
|
||||||
|
errorMessage: PropTypes.string,
|
||||||
|
isRequired: PropTypes.bool,
|
||||||
|
value: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.bool,
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FormFieldRenderer;
|
export default FormFieldRenderer;
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export { default } from './FieldRenderer';
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
export { default as FormFieldRenderer } from './FieldRenderer';
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
|
|
||||||
import FieldRenderer from '../FieldRenderer';
|
import FieldRenderer from '../FieldRenderer';
|
||||||
@@ -23,7 +25,7 @@ describe('FieldRendererTests', () => {
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
label: 'Year of Birth',
|
label: 'Year of Birth',
|
||||||
name: 'yob-field',
|
name: 'yob-field',
|
||||||
options: [['1997', 1997], ['1998', 1998]],
|
options: [['1997', '1997'], ['1998', '1998']],
|
||||||
};
|
};
|
||||||
|
|
||||||
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
|
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
|
||||||
@@ -81,7 +83,7 @@ describe('FieldRendererTests', () => {
|
|||||||
it('should render checkbox field', () => {
|
it('should render checkbox field', () => {
|
||||||
const fieldData = {
|
const fieldData = {
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
label: 'I agree that edX may send me marketing messages.',
|
label: `I agree that ${getConfig().SITE_NAME} may send me marketing messages.`,
|
||||||
name: 'marketing-emails-opt-in-field',
|
name: 'marketing-emails-opt-in-field',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -90,7 +92,7 @@ describe('FieldRendererTests', () => {
|
|||||||
field.simulate('change', { target: { checked: true, type: 'checkbox' } });
|
field.simulate('change', { target: { checked: true, type: 'checkbox' } });
|
||||||
|
|
||||||
expect(field.prop('type')).toEqual('checkbox');
|
expect(field.prop('type')).toEqual('checkbox');
|
||||||
expect(fieldRenderer.find('label').text()).toEqual('I agree that edX may send me marketing messages.');
|
expect(fieldRenderer.find('label').text()).toEqual(fieldData.label);
|
||||||
expect(value).toEqual(true);
|
expect(value).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,4 +104,96 @@ describe('FieldRendererTests', () => {
|
|||||||
const fieldRenderer = mount(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
|
const fieldRenderer = mount(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
|
||||||
expect(fieldRenderer.html()).toBeNull();
|
expect(fieldRenderer.html()).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should run onBlur and onFocus functions for a field if given', () => {
|
||||||
|
const fieldData = { type: 'text', label: 'Test', name: 'test-field' };
|
||||||
|
let functionValue = '';
|
||||||
|
|
||||||
|
const onBlur = (e) => {
|
||||||
|
functionValue = `${e.target.name} blurred`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFocus = (e) => {
|
||||||
|
functionValue = `${e.target.name} focussed`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldRenderer = mount(
|
||||||
|
<FieldRenderer
|
||||||
|
handleFocus={onFocus}
|
||||||
|
handleBlur={onBlur}
|
||||||
|
value={value}
|
||||||
|
fieldData={fieldData}
|
||||||
|
onChangeHandler={changeHandler}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const field = fieldRenderer.find('#test-field').last();
|
||||||
|
|
||||||
|
field.simulate('focus');
|
||||||
|
expect(functionValue).toEqual('test-field focussed');
|
||||||
|
|
||||||
|
field.simulate('blur');
|
||||||
|
expect(functionValue).toEqual('test-field blurred');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render error message for required text fields', () => {
|
||||||
|
const fieldData = { type: 'text', label: 'First Name', name: 'first-name-field' };
|
||||||
|
|
||||||
|
const fieldRenderer = mount(
|
||||||
|
<FieldRenderer
|
||||||
|
isRequired
|
||||||
|
fieldData={fieldData}
|
||||||
|
onChangeHandler={changeHandler}
|
||||||
|
errorMessage="Enter your first name"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('Enter your first name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render error message for required select fields', () => {
|
||||||
|
const fieldData = {
|
||||||
|
type: 'select', label: 'Preference', name: 'preference-field', options: [['a', 'Opt 1'], ['b', 'Opt 2']],
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldRenderer = mount(
|
||||||
|
<FieldRenderer
|
||||||
|
isRequired
|
||||||
|
fieldData={fieldData}
|
||||||
|
onChangeHandler={changeHandler}
|
||||||
|
errorMessage="Select your preference"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('Select your preference');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render error message for required textarea fields', () => {
|
||||||
|
const fieldData = { type: 'textarea', label: 'Goals', name: 'goals-field' };
|
||||||
|
|
||||||
|
const fieldRenderer = mount(
|
||||||
|
<FieldRenderer
|
||||||
|
isRequired
|
||||||
|
fieldData={fieldData}
|
||||||
|
onChangeHandler={changeHandler}
|
||||||
|
errorMessage="Tell us your goals"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('Tell us your goals');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render error message for required checkbox fields', () => {
|
||||||
|
const fieldData = { type: 'checkbox', label: 'Honor Code', name: 'honor-code-field' };
|
||||||
|
|
||||||
|
const fieldRenderer = mount(
|
||||||
|
<FieldRenderer
|
||||||
|
isRequired
|
||||||
|
fieldData={fieldData}
|
||||||
|
onChangeHandler={changeHandler}
|
||||||
|
errorMessage="You must agree to our Honor Code"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('You must agree to our Honor Code');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,29 +1,31 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Alert } from '@edx/paragon';
|
import { Alert } from '@edx/paragon';
|
||||||
import { CheckCircle, Error } from '@edx/paragon/icons';
|
import { CheckCircle, Error } from '@edx/paragon/icons';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { INTERNAL_SERVER_ERROR } from '../data/constants';
|
import {
|
||||||
|
COMPLETE_STATE, FORBIDDEN_STATE, FORM_SUBMISSION_ERROR, INTERNAL_SERVER_ERROR,
|
||||||
|
} from '../data/constants';
|
||||||
import { PASSWORD_RESET } from '../reset-password/data/constants';
|
import { PASSWORD_RESET } from '../reset-password/data/constants';
|
||||||
|
|
||||||
const ForgotPasswordAlert = (props) => {
|
const ForgotPasswordAlert = (props) => {
|
||||||
const { email, emailError, intl } = props;
|
const { formatMessage } = useIntl();
|
||||||
|
const { email, emailError } = props;
|
||||||
|
let message = '';
|
||||||
|
let heading = formatMessage(messages['forgot.password.error.alert.title']);
|
||||||
let { status } = props;
|
let { status } = props;
|
||||||
|
|
||||||
if (emailError) {
|
if (emailError) {
|
||||||
status = 'form-submission-error';
|
status = FORM_SUBMISSION_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = '';
|
|
||||||
let heading = intl.formatMessage(messages['forgot.password.error.alert.title']);
|
|
||||||
const supportUrl = getConfig().PASSWORD_RESET_SUPPORT_LINK;
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'complete':
|
case COMPLETE_STATE:
|
||||||
heading = intl.formatMessage(messages['confirmation.message.title']);
|
heading = formatMessage(messages['confirmation.message.title']);
|
||||||
message = (
|
message = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="forgot.password.confirmation.message"
|
id="forgot.password.confirmation.message"
|
||||||
@@ -34,15 +36,8 @@ const ForgotPasswordAlert = (props) => {
|
|||||||
values={{
|
values={{
|
||||||
email: <span className="data-hj-suppress">{email}</span>,
|
email: <span className="data-hj-suppress">{email}</span>,
|
||||||
supportLink: (
|
supportLink: (
|
||||||
<Alert.Link
|
<Alert.Link href={getConfig().PASSWORD_RESET_SUPPORT_LINK} target="_blank">
|
||||||
className="alert-link"
|
{formatMessage(messages['confirmation.support.link'])}
|
||||||
href={supportUrl}
|
|
||||||
onClick={e => {
|
|
||||||
e.preventDefault();
|
|
||||||
window.open(supportUrl, '_blank');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{intl.formatMessage(messages['confirmation.support.link'])}
|
|
||||||
</Alert.Link>
|
</Alert.Link>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
@@ -50,26 +45,26 @@ const ForgotPasswordAlert = (props) => {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case INTERNAL_SERVER_ERROR:
|
case INTERNAL_SERVER_ERROR:
|
||||||
message = intl.formatMessage(messages['internal.server.error']);
|
message = formatMessage(messages['internal.server.error']);
|
||||||
break;
|
break;
|
||||||
case 'forbidden':
|
case FORBIDDEN_STATE:
|
||||||
heading = intl.formatMessage(messages['forgot.password.error.message.title']);
|
heading = formatMessage(messages['forgot.password.error.message.title']);
|
||||||
message = intl.formatMessage(messages['forgot.password.request.in.progress.message']);
|
message = formatMessage(messages['forgot.password.request.in.progress.message']);
|
||||||
break;
|
break;
|
||||||
case 'form-submission-error':
|
case FORM_SUBMISSION_ERROR:
|
||||||
message = intl.formatMessage(messages['extend.field.errors'], { emailError });
|
message = formatMessage(messages['extend.field.errors'], { emailError });
|
||||||
break;
|
break;
|
||||||
case PASSWORD_RESET.INVALID_TOKEN:
|
case PASSWORD_RESET.INVALID_TOKEN:
|
||||||
heading = intl.formatMessage(messages['invalid.token.heading']);
|
heading = formatMessage(messages['invalid.token.heading']);
|
||||||
message = intl.formatMessage(messages['invalid.token.error.message']);
|
message = formatMessage(messages['invalid.token.error.message']);
|
||||||
break;
|
break;
|
||||||
case PASSWORD_RESET.FORBIDDEN_REQUEST:
|
case PASSWORD_RESET.FORBIDDEN_REQUEST:
|
||||||
heading = intl.formatMessage(messages['token.validation.rate.limit.error.heading']);
|
heading = formatMessage(messages['token.validation.rate.limit.error.heading']);
|
||||||
message = intl.formatMessage(messages['token.validation.rate.limit.error']);
|
message = formatMessage(messages['token.validation.rate.limit.error']);
|
||||||
break;
|
break;
|
||||||
case PASSWORD_RESET.INTERNAL_SERVER_ERROR:
|
case PASSWORD_RESET.INTERNAL_SERVER_ERROR:
|
||||||
heading = intl.formatMessage(messages['token.validation.internal.sever.error.heading']);
|
heading = formatMessage(messages['token.validation.internal.sever.error.heading']);
|
||||||
message = intl.formatMessage(messages['token.validation.internal.sever.error']);
|
message = formatMessage(messages['token.validation.internal.sever.error']);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -79,7 +74,7 @@ const ForgotPasswordAlert = (props) => {
|
|||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
id="validation-errors"
|
id="validation-errors"
|
||||||
className="mb-5"
|
className="mb-4"
|
||||||
variant={`${status === 'complete' ? 'success' : 'danger'}`}
|
variant={`${status === 'complete' ? 'success' : 'danger'}`}
|
||||||
icon={status === 'complete' ? CheckCircle : Error}
|
icon={status === 'complete' ? CheckCircle : Error}
|
||||||
>
|
>
|
||||||
@@ -99,8 +94,7 @@ ForgotPasswordAlert.defaultProps = {
|
|||||||
ForgotPasswordAlert.propTypes = {
|
ForgotPasswordAlert.propTypes = {
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
email: PropTypes.string,
|
email: PropTypes.string,
|
||||||
intl: intlShape.isRequired,
|
|
||||||
emailError: PropTypes.string,
|
emailError: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(ForgotPasswordAlert);
|
export default ForgotPasswordAlert;
|
||||||
|
|||||||
@@ -1,149 +1,166 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Formik } from 'formik';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Helmet } from 'react-helmet';
|
|
||||||
import { Redirect } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
StatefulButton,
|
|
||||||
Hyperlink,
|
Hyperlink,
|
||||||
Tabs,
|
|
||||||
Tab,
|
|
||||||
Icon,
|
Icon,
|
||||||
|
StatefulButton,
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
} from '@edx/paragon';
|
} from '@edx/paragon';
|
||||||
import { ChevronLeft } from '@edx/paragon/icons';
|
import { ChevronLeft } from '@edx/paragon/icons';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { Redirect } from 'react-router-dom';
|
||||||
|
|
||||||
import { forgotPassword } from './data/actions';
|
import { forgotPassword, setForgotPasswordFormData } from './data/actions';
|
||||||
import { forgotPasswordResultSelector } from './data/selectors';
|
import { forgotPasswordResultSelector } from './data/selectors';
|
||||||
|
import ForgotPasswordAlert from './ForgotPasswordAlert';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import { BaseComponent } from '../base-component';
|
||||||
import { FormGroup } from '../common-components';
|
import { FormGroup } from '../common-components';
|
||||||
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
|
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
|
||||||
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
||||||
import ForgotPasswordAlert from './ForgotPasswordAlert';
|
|
||||||
import BaseComponent from '../base-component';
|
|
||||||
|
|
||||||
const ForgotPasswordPage = (props) => {
|
const ForgotPasswordPage = (props) => {
|
||||||
const { intl, status, submitState } = props;
|
|
||||||
|
|
||||||
const platformName = getConfig().SITE_NAME;
|
const platformName = getConfig().SITE_NAME;
|
||||||
const regex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
||||||
const [validationError, setValidationError] = useState('');
|
const {
|
||||||
|
status, submitState, emailValidationError,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const [email, setEmail] = useState(props.email);
|
||||||
|
const [bannerEmail, setBannerEmail] = useState('');
|
||||||
|
const [formErrors, setFormErrors] = useState('');
|
||||||
|
const [validationError, setValidationError] = useState(emailValidationError);
|
||||||
const [key, setKey] = useState('');
|
const [key, setKey] = useState('');
|
||||||
const supportUrl = getConfig().LOGIN_ISSUE_SUPPORT_LINK;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sendPageEvent('login_and_registration', 'reset');
|
sendPageEvent('login_and_registration', 'reset');
|
||||||
sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' });
|
sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getValidationMessage = (email) => {
|
useEffect(() => {
|
||||||
|
setValidationError(emailValidationError);
|
||||||
|
}, [emailValidationError]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status === 'complete') {
|
||||||
|
setEmail('');
|
||||||
|
}
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
|
const getValidationMessage = (value) => {
|
||||||
let error = '';
|
let error = '';
|
||||||
|
|
||||||
if (email === '') {
|
if (value === '') {
|
||||||
error = intl.formatMessage(messages['forgot.password.empty.email.field.error']);
|
error = formatMessage(messages['forgot.password.empty.email.field.error']);
|
||||||
} else if (!regex.test(email)) {
|
} else if (!emailRegex.test(value)) {
|
||||||
error = intl.formatMessage(messages['forgot.password.page.invalid.email.message']);
|
error = formatMessage(messages['forgot.password.page.invalid.email.message']);
|
||||||
}
|
}
|
||||||
|
|
||||||
setValidationError(error);
|
|
||||||
return error;
|
return error;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
props.setForgotPasswordFormData({ email, emailValidationError: getValidationMessage(email) });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = () => props.setForgotPasswordFormData({ emailValidationError: '' });
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setBannerEmail(email);
|
||||||
|
|
||||||
|
const error = getValidationMessage(email);
|
||||||
|
if (error) {
|
||||||
|
setFormErrors(error);
|
||||||
|
props.setForgotPasswordFormData({ email, emailValidationError: error });
|
||||||
|
windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
|
||||||
|
} else {
|
||||||
|
props.forgotPassword(email);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const tabTitle = (
|
const tabTitle = (
|
||||||
<div className="d-flex">
|
<div className="d-inline-flex flex-wrap align-items-center">
|
||||||
<Icon src={ChevronLeft} className="arrow-back-icon" />
|
<Icon src={ChevronLeft} />
|
||||||
<span className="ml-2">{intl.formatMessage(messages['sign.in.text'])}</span>
|
<span className="ml-2">{formatMessage(messages['sign.in.text'])}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseComponent>
|
<BaseComponent>
|
||||||
|
<Helmet>
|
||||||
|
<title>{formatMessage(messages['forgot.password.page.title'],
|
||||||
|
{ siteName: getConfig().SITE_NAME })}
|
||||||
|
</title>
|
||||||
|
</Helmet>
|
||||||
<div>
|
<div>
|
||||||
<Tabs activeKey="" id="controlled-tab-example" onSelect={(k) => setKey(k)}>
|
<Tabs activeKey="" id="controlled-tab" onSelect={(k) => setKey(k)}>
|
||||||
<Tab title={tabTitle} eventKey={LOGIN_PAGE} />
|
<Tab title={tabTitle} eventKey={LOGIN_PAGE} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{ key && (
|
{ key && (
|
||||||
<Redirect to={updatePathWithQueryParams(key)} />
|
<Redirect to={updatePathWithQueryParams(key)} />
|
||||||
)}
|
)}
|
||||||
<div id="main-content" className="main-content">
|
<div id="main-content" className="main-content">
|
||||||
<Formik
|
<Form id="forget-password-form" name="forget-password-form" className="mw-xs">
|
||||||
initialValues={{ email: '' }}
|
<ForgotPasswordAlert email={bannerEmail} emailError={formErrors} status={status} />
|
||||||
validateOnChange={false}
|
<h2 className="h4">
|
||||||
validate={(values) => {
|
{formatMessage(messages['forgot.password.page.heading'])}
|
||||||
const validationMessage = getValidationMessage(values.email);
|
</h2>
|
||||||
|
<p className="mb-4">
|
||||||
if (validationMessage !== '') {
|
{formatMessage(messages['forgot.password.page.instructions'])}
|
||||||
windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
|
</p>
|
||||||
return { email: validationMessage };
|
<FormGroup
|
||||||
}
|
floatingLabel={formatMessage(messages['forgot.password.page.email.field.label'])}
|
||||||
|
name="email"
|
||||||
return {};
|
value={email}
|
||||||
}}
|
autoComplete="on"
|
||||||
onSubmit={(values) => { props.forgotPassword(values.email); }}
|
errorMessage={validationError}
|
||||||
>
|
handleChange={(e) => setEmail(e.target.value)}
|
||||||
{({
|
handleBlur={handleBlur}
|
||||||
errors, handleSubmit, setFieldValue, values,
|
handleFocus={handleFocus}
|
||||||
}) => (
|
helpText={[formatMessage(messages['forgot.password.email.help.text'], { platformName })]}
|
||||||
<>
|
/>
|
||||||
<Helmet>
|
<StatefulButton
|
||||||
<title>{intl.formatMessage(messages['forgot.password.page.title'],
|
id="submit-forget-password"
|
||||||
{ siteName: getConfig().SITE_NAME })}
|
name="submit-forget-password"
|
||||||
</title>
|
type="submit"
|
||||||
</Helmet>
|
variant="brand"
|
||||||
<Form className="mw-xs">
|
className="forgot-password--button"
|
||||||
<ForgotPasswordAlert email={props.email} emailError={errors.email} status={status} />
|
state={submitState}
|
||||||
<h4>
|
labels={{
|
||||||
{intl.formatMessage(messages['forgot.password.page.heading'])}
|
default: formatMessage(messages['forgot.password.page.submit.button']),
|
||||||
</h4>
|
pending: '',
|
||||||
<p className="mb-4">
|
}}
|
||||||
{intl.formatMessage(messages['forgot.password.page.instructions'])}
|
onClick={handleSubmit}
|
||||||
</p>
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
<FormGroup
|
/>
|
||||||
floatingLabel={intl.formatMessage(messages['forgot.password.page.email.field.label'])}
|
{(getConfig().LOGIN_ISSUE_SUPPORT_LINK) && (
|
||||||
name="email"
|
<Hyperlink
|
||||||
errorMessage={validationError}
|
id="forgot-password"
|
||||||
value={values.email}
|
name="forgot-password"
|
||||||
handleBlur={() => getValidationMessage(values.email)}
|
className="ml-4 font-weight-500 text-body"
|
||||||
handleChange={e => setFieldValue('email', e.target.value)}
|
destination={getConfig().LOGIN_ISSUE_SUPPORT_LINK}
|
||||||
handleFocus={() => setValidationError('')}
|
target="_blank"
|
||||||
helpText={[intl.formatMessage(messages['forgot.password.email.help.text'], { platformName })]}
|
showLaunchIcon={false}
|
||||||
/>
|
>
|
||||||
<StatefulButton
|
{formatMessage(messages['need.help.sign.in.text'])}
|
||||||
type="submit"
|
</Hyperlink>
|
||||||
variant="brand"
|
|
||||||
className="login-button-width"
|
|
||||||
state={submitState}
|
|
||||||
labels={{
|
|
||||||
default: intl.formatMessage(messages['forgot.password.page.submit.button']),
|
|
||||||
pending: '',
|
|
||||||
}}
|
|
||||||
onClick={handleSubmit}
|
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
|
||||||
/>
|
|
||||||
<Hyperlink
|
|
||||||
id="forgot-password"
|
|
||||||
className="ml-4 font-weight-500 text-body"
|
|
||||||
destination={supportUrl}
|
|
||||||
onClick={e => {
|
|
||||||
e.preventDefault();
|
|
||||||
window.open(supportUrl, '_blank');
|
|
||||||
}}
|
|
||||||
>{intl.formatMessage(messages['need.help.sign.in.text'])}
|
|
||||||
</Hyperlink>
|
|
||||||
<p className="mt-5 one-rem-font">{intl.formatMessage(messages['additional.help.text'])}
|
|
||||||
<span><Hyperlink isInline destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink></span>
|
|
||||||
</p>
|
|
||||||
</Form>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Formik>
|
<p className="mt-5.5 small text-gray-700">
|
||||||
|
{formatMessage(messages['additional.help.text'], { platformName })}
|
||||||
|
<span>
|
||||||
|
<Hyperlink isInline destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseComponent>
|
</BaseComponent>
|
||||||
@@ -151,15 +168,17 @@ const ForgotPasswordPage = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ForgotPasswordPage.propTypes = {
|
ForgotPasswordPage.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
|
||||||
email: PropTypes.string,
|
email: PropTypes.string,
|
||||||
|
emailValidationError: PropTypes.string,
|
||||||
forgotPassword: PropTypes.func.isRequired,
|
forgotPassword: PropTypes.func.isRequired,
|
||||||
|
setForgotPasswordFormData: PropTypes.func.isRequired,
|
||||||
status: PropTypes.string,
|
status: PropTypes.string,
|
||||||
submitState: PropTypes.string,
|
submitState: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
ForgotPasswordPage.defaultProps = {
|
ForgotPasswordPage.defaultProps = {
|
||||||
email: '',
|
email: '',
|
||||||
|
emailValidationError: '',
|
||||||
status: null,
|
status: null,
|
||||||
submitState: DEFAULT_STATE,
|
submitState: DEFAULT_STATE,
|
||||||
};
|
};
|
||||||
@@ -168,5 +187,6 @@ export default connect(
|
|||||||
forgotPasswordResultSelector,
|
forgotPasswordResultSelector,
|
||||||
{
|
{
|
||||||
forgotPassword,
|
forgotPassword,
|
||||||
|
setForgotPasswordFormData,
|
||||||
},
|
},
|
||||||
)(injectIntl(ForgotPasswordPage));
|
)(ForgotPasswordPage);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AsyncActionType } from '../../data/utils';
|
import { AsyncActionType } from '../../data/utils';
|
||||||
|
|
||||||
export const FORGOT_PASSWORD = new AsyncActionType('FORGOT', 'PASSWORD');
|
export const FORGOT_PASSWORD = new AsyncActionType('FORGOT', 'PASSWORD');
|
||||||
|
export const FORGOT_PASSWORD_PERSIST_FORM_DATA = 'FORGOT_PASSWORD_PERSIST_FORM_DATA';
|
||||||
|
|
||||||
// Forgot Password
|
// Forgot Password
|
||||||
export const forgotPassword = email => ({
|
export const forgotPassword = email => ({
|
||||||
@@ -24,3 +25,8 @@ export const forgotPasswordForbidden = () => ({
|
|||||||
export const forgotPasswordServerError = () => ({
|
export const forgotPasswordServerError = () => ({
|
||||||
type: FORGOT_PASSWORD.FAILURE,
|
type: FORGOT_PASSWORD.FAILURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const setForgotPasswordFormData = (forgotPasswordFormData) => ({
|
||||||
|
type: FORGOT_PASSWORD_PERSIST_FORM_DATA,
|
||||||
|
payload: { forgotPasswordFormData },
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FORGOT_PASSWORD } from './actions';
|
import { FORGOT_PASSWORD, FORGOT_PASSWORD_PERSIST_FORM_DATA } from './actions';
|
||||||
import { INTERNAL_SERVER_ERROR, PENDING_STATE } from '../../data/constants';
|
import { INTERNAL_SERVER_ERROR, PENDING_STATE } from '../../data/constants';
|
||||||
import { PASSWORD_RESET_FAILURE } from '../../reset-password/data/actions';
|
import { PASSWORD_RESET_FAILURE } from '../../reset-password/data/actions';
|
||||||
|
|
||||||
@@ -6,6 +6,7 @@ export const defaultState = {
|
|||||||
status: '',
|
status: '',
|
||||||
submitState: '',
|
submitState: '',
|
||||||
email: '',
|
email: '',
|
||||||
|
emailValidationError: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const reducer = (state = defaultState, action = null) => {
|
const reducer = (state = defaultState, action = null) => {
|
||||||
@@ -13,28 +14,42 @@ const reducer = (state = defaultState, action = null) => {
|
|||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case FORGOT_PASSWORD.BEGIN:
|
case FORGOT_PASSWORD.BEGIN:
|
||||||
return {
|
return {
|
||||||
|
email: state.email,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
submitState: PENDING_STATE,
|
submitState: PENDING_STATE,
|
||||||
};
|
};
|
||||||
case FORGOT_PASSWORD.SUCCESS:
|
case FORGOT_PASSWORD.SUCCESS:
|
||||||
return {
|
return {
|
||||||
...action.payload,
|
...defaultState,
|
||||||
status: 'complete',
|
status: 'complete',
|
||||||
};
|
};
|
||||||
case FORGOT_PASSWORD.FORBIDDEN:
|
case FORGOT_PASSWORD.FORBIDDEN:
|
||||||
return {
|
return {
|
||||||
|
email: state.email,
|
||||||
status: 'forbidden',
|
status: 'forbidden',
|
||||||
};
|
};
|
||||||
case FORGOT_PASSWORD.FAILURE:
|
case FORGOT_PASSWORD.FAILURE:
|
||||||
return {
|
return {
|
||||||
|
email: state.email,
|
||||||
status: INTERNAL_SERVER_ERROR,
|
status: INTERNAL_SERVER_ERROR,
|
||||||
};
|
};
|
||||||
case PASSWORD_RESET_FAILURE:
|
case PASSWORD_RESET_FAILURE:
|
||||||
return {
|
return {
|
||||||
status: action.payload.errorCode,
|
status: action.payload.errorCode,
|
||||||
};
|
};
|
||||||
|
case FORGOT_PASSWORD_PERSIST_FORM_DATA: {
|
||||||
|
const { forgotPasswordFormData } = action.payload;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...forgotPasswordFormData,
|
||||||
|
};
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return defaultState;
|
return {
|
||||||
|
...defaultState,
|
||||||
|
email: state.email,
|
||||||
|
emailValidationError: state.emailValidationError,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ import { call, put, takeEvery } from 'redux-saga/effects';
|
|||||||
import {
|
import {
|
||||||
FORGOT_PASSWORD,
|
FORGOT_PASSWORD,
|
||||||
forgotPasswordBegin,
|
forgotPasswordBegin,
|
||||||
forgotPasswordSuccess,
|
|
||||||
forgotPasswordForbidden,
|
forgotPasswordForbidden,
|
||||||
forgotPasswordServerError,
|
forgotPasswordServerError,
|
||||||
|
forgotPasswordSuccess,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
|
|
||||||
import { forgotPassword } from './service';
|
import { forgotPassword } from './service';
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
|
|||||||
34
src/forgot-password/data/tests/reducers.test.js
Normal file
34
src/forgot-password/data/tests/reducers.test.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {
|
||||||
|
FORGOT_PASSWORD_PERSIST_FORM_DATA,
|
||||||
|
} from '../actions';
|
||||||
|
import reducer from '../reducers';
|
||||||
|
|
||||||
|
describe('forgot password reducer', () => {
|
||||||
|
it('should set email and emailValidationError', () => {
|
||||||
|
const state = {
|
||||||
|
status: '',
|
||||||
|
submitState: '',
|
||||||
|
email: '',
|
||||||
|
emailValidationError: '',
|
||||||
|
};
|
||||||
|
const forgotPasswordFormData = {
|
||||||
|
email: 'test@gmail',
|
||||||
|
emailValidationError: 'Enter a valid email address',
|
||||||
|
};
|
||||||
|
const action = {
|
||||||
|
type: FORGOT_PASSWORD_PERSIST_FORM_DATA,
|
||||||
|
payload: { forgotPasswordFormData },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
reducer(state, action),
|
||||||
|
).toEqual(
|
||||||
|
{
|
||||||
|
status: '',
|
||||||
|
submitState: '',
|
||||||
|
email: 'test@gmail',
|
||||||
|
emailValidationError: 'Enter a valid email address',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import { runSaga } from 'redux-saga';
|
import { runSaga } from 'redux-saga';
|
||||||
|
|
||||||
|
import initializeMockLogging from '../../../setupTest';
|
||||||
import * as actions from '../actions';
|
import * as actions from '../actions';
|
||||||
import { handleForgotPassword } from '../sagas';
|
import { handleForgotPassword } from '../sagas';
|
||||||
import * as api from '../service';
|
import * as api from '../service';
|
||||||
import initializeMockLogging from '../../../setupTest';
|
|
||||||
|
|
||||||
const { loggingService } = initializeMockLogging();
|
const { loggingService } = initializeMockLogging();
|
||||||
|
|
||||||
describe('handleForgotPassword', () => {
|
describe('handleForgotPassword', () => {
|
||||||
const params = {
|
const params = {
|
||||||
payload: {
|
payload: {
|
||||||
formData: {
|
forgotPasswordFormData: {
|
||||||
email: 'test@test.com',
|
email: 'test@test.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export { default } from './ForgotPasswordPage';
|
export { default as ForgotPasswordPage } from './ForgotPasswordPage';
|
||||||
export { default as reducer } from './data/reducers';
|
export { default as reducer } from './data/reducers';
|
||||||
export { FORGOT_PASSWORD } from './data/actions';
|
export { FORGOT_PASSWORD } from './data/actions';
|
||||||
export { default as saga } from './data/sagas';
|
export { default as saga } from './data/sagas';
|
||||||
|
|||||||
@@ -51,16 +51,6 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Enter your email',
|
defaultMessage: 'Enter your email',
|
||||||
description: 'Error message that appears when user tries to submit empty email field',
|
description: 'Error message that appears when user tries to submit empty email field',
|
||||||
},
|
},
|
||||||
'forgot.password.invalid.email.heading': {
|
|
||||||
id: 'forgot.password.invalid.email',
|
|
||||||
defaultMessage: 'An error occurred.',
|
|
||||||
description: 'heading for invalid email',
|
|
||||||
},
|
|
||||||
'forgot.password.invalid.email.message': {
|
|
||||||
id: 'forgot.password.invalid.email.message',
|
|
||||||
defaultMessage: "The email address you've provided isn't formatted correctly.",
|
|
||||||
description: 'message for invalid email',
|
|
||||||
},
|
|
||||||
'forgot.password.email.help.text': {
|
'forgot.password.email.help.text': {
|
||||||
id: 'forgot.password.email.help.text',
|
id: 'forgot.password.email.help.text',
|
||||||
defaultMessage: 'The email address you used to register with {platformName}',
|
defaultMessage: 'The email address you used to register with {platformName}',
|
||||||
@@ -84,7 +74,7 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
'additional.help.text': {
|
'additional.help.text': {
|
||||||
id: 'additional.help.text',
|
id: 'additional.help.text',
|
||||||
defaultMessage: 'For additional help, contact edX support at ',
|
defaultMessage: 'For additional help, contact {platformName} support at ',
|
||||||
description: 'additional help text on forgot password page',
|
description: 'additional help text on forgot password page',
|
||||||
},
|
},
|
||||||
'sign.in.text': {
|
'sign.in.text': {
|
||||||
@@ -134,10 +124,5 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'An error has occurred. Try refreshing the page, or check your internet connection.',
|
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',
|
description: 'Error message that appears when server responds with 500 error code',
|
||||||
},
|
},
|
||||||
'rate.limit.error': {
|
|
||||||
id: 'rate.limit.error',
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
@@ -1,27 +1,25 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { mount } from 'enzyme';
|
|
||||||
import { act } from 'react-dom/test-utils';
|
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import { mergeConfig } from '@edx/frontend-platform';
|
||||||
|
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
import { MemoryRouter, Router } from 'react-router-dom';
|
import { MemoryRouter, Router } from 'react-router-dom';
|
||||||
import configureStore from 'redux-mock-store';
|
import configureStore from 'redux-mock-store';
|
||||||
import { createMemoryHistory } from 'history';
|
|
||||||
|
|
||||||
import * as analytics from '@edx/frontend-platform/analytics';
|
|
||||||
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
|
|
||||||
import { mergeConfig } from '@edx/frontend-platform';
|
|
||||||
import { IntlProvider, injectIntl, configure } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
import * as auth from '@edx/frontend-platform/auth';
|
|
||||||
import ForgotPasswordPage from '../ForgotPasswordPage';
|
|
||||||
import { INTERNAL_SERVER_ERROR, LOGIN_PAGE } from '../../data/constants';
|
import { INTERNAL_SERVER_ERROR, LOGIN_PAGE } from '../../data/constants';
|
||||||
import { PASSWORD_RESET } from '../../reset-password/data/constants';
|
import { PASSWORD_RESET } from '../../reset-password/data/constants';
|
||||||
|
import { setForgotPasswordFormData } from '../data/actions';
|
||||||
|
import ForgotPasswordPage from '../ForgotPasswordPage';
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/analytics');
|
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||||
|
sendPageEvent: jest.fn(),
|
||||||
|
sendTrackEvent: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('@edx/frontend-platform/auth');
|
jest.mock('@edx/frontend-platform/auth');
|
||||||
|
|
||||||
analytics.sendPageEvent = jest.fn();
|
|
||||||
|
|
||||||
const IntlForgotPasswordPage = injectIntl(ForgotPasswordPage);
|
const IntlForgotPasswordPage = injectIntl(ForgotPasswordPage);
|
||||||
const mockStore = configureStore();
|
const mockStore = configureStore();
|
||||||
const history = createMemoryHistory();
|
const history = createMemoryHistory();
|
||||||
@@ -51,7 +49,12 @@ describe('ForgotPasswordPage', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
store = mockStore(initialState);
|
store = mockStore(initialState);
|
||||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'edX' }));
|
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||||
|
getAuthenticatedUser: jest.fn(() => ({
|
||||||
|
userId: 3,
|
||||||
|
username: 'test-user',
|
||||||
|
})),
|
||||||
|
}));
|
||||||
configure({
|
configure({
|
||||||
loggingService: { logError: jest.fn() },
|
loggingService: { logError: jest.fn() },
|
||||||
config: {
|
config: {
|
||||||
@@ -66,7 +69,15 @@ describe('ForgotPasswordPage', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('not should display need other help signing in button', () => {
|
||||||
|
const wrapper = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||||
|
expect(wrapper.find('#forgot-password').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
it('should display need other help signing in button', () => {
|
it('should display need other help signing in button', () => {
|
||||||
|
mergeConfig({
|
||||||
|
LOGIN_ISSUE_SUPPORT_LINK: '/support',
|
||||||
|
});
|
||||||
const wrapper = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
const wrapper = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||||
expect(wrapper.find('#forgot-password').first().text()).toEqual('Need help signing in?');
|
expect(wrapper.find('#forgot-password').first().text()).toEqual('Need help signing in?');
|
||||||
});
|
});
|
||||||
@@ -125,34 +136,62 @@ describe('ForgotPasswordPage', () => {
|
|||||||
expect(forgotPasswordPage.find('#email-invalid-feedback').exists()).toEqual(false);
|
expect(forgotPasswordPage.find('#email-invalid-feedback').exists()).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display error message on blur event', async () => {
|
it('should set error in redux store on onBlur', () => {
|
||||||
const validationMessage = 'Enter your email';
|
const forgotPasswordFormData = {
|
||||||
|
email: 'test@gmail',
|
||||||
|
emailValidationError: 'Enter a valid email address',
|
||||||
|
};
|
||||||
|
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
email: 'test@gmail',
|
||||||
|
emailValidationError: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
store.dispatch = jest.fn(store.dispatch);
|
||||||
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||||
const emailInput = forgotPasswordPage.find('input#email');
|
forgotPasswordPage.find('input#email').simulate('blur');
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(setForgotPasswordFormData(forgotPasswordFormData));
|
||||||
await act(async () => {
|
});
|
||||||
await emailInput.simulate('blur', { target: { value: '', name: 'email' } });
|
|
||||||
});
|
|
||||||
|
|
||||||
|
it('should display error message if available in props', async () => {
|
||||||
|
const validationMessage = 'Enter your email';
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
emailValidationError: validationMessage,
|
||||||
|
email: '',
|
||||||
|
};
|
||||||
|
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||||
forgotPasswordPage.update();
|
forgotPasswordPage.update();
|
||||||
expect(forgotPasswordPage.find('.pgn__form-text-invalid').text()).toEqual(validationMessage);
|
expect(forgotPasswordPage.find('.pgn__form-text-invalid').text()).toEqual(validationMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should clear error message on focus event', async () => {
|
it('should clear error in redux store on onFocus', () => {
|
||||||
const validationMessage = 'Enter your email';
|
const forgotPasswordFormData = {
|
||||||
|
emailValidationError: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
email: 'test@gmail',
|
||||||
|
emailValidationError: 'Enter a valid email address',
|
||||||
|
};
|
||||||
|
|
||||||
|
store.dispatch = jest.fn(store.dispatch);
|
||||||
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||||
await act(async () => { await forgotPasswordPage.find('button.btn-brand').simulate('click'); });
|
|
||||||
|
|
||||||
forgotPasswordPage.update();
|
|
||||||
expect(forgotPasswordPage.find('.pgn__form-text-invalid').text()).toEqual(validationMessage);
|
|
||||||
|
|
||||||
forgotPasswordPage.find('input#email').simulate('focus');
|
forgotPasswordPage.find('input#email').simulate('focus');
|
||||||
expect(forgotPasswordPage.find('#email-invalid-feedback').exists()).toEqual(false);
|
expect(store.dispatch).toHaveBeenCalledWith(setForgotPasswordFormData(forgotPasswordFormData));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('check cookie rendered', () => {
|
it('should clear error message when cleared in props on focus', async () => {
|
||||||
const forgotPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
props = {
|
||||||
expect(forgotPage.find(<CookiePolicyBanner />)).toBeTruthy();
|
...props,
|
||||||
|
emailValidationError: '',
|
||||||
|
email: '',
|
||||||
|
};
|
||||||
|
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||||
|
forgotPasswordPage.update();
|
||||||
|
expect(forgotPasswordPage.find('#email-invalid-feedback').exists()).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display success message after email is sent', () => {
|
it('should display success message after email is sent', () => {
|
||||||
|
|||||||
@@ -1,41 +1,39 @@
|
|||||||
|
import { messages as paragonMessages } from '@edx/paragon';
|
||||||
|
|
||||||
import arMessages from './messages/ar.json';
|
import arMessages from './messages/ar.json';
|
||||||
import caMessages from './messages/ca.json';
|
import deMessages from './messages/de.json';
|
||||||
// no need to import en messages-- they are in the defaultMessage field
|
import dedeCAMessages from './messages/de_DE.json';
|
||||||
import es419Messages from './messages/es_419.json';
|
import es419Messages from './messages/es_419.json';
|
||||||
import frMessages from './messages/fr.json';
|
import frMessages from './messages/fr.json';
|
||||||
import zhcnMessages from './messages/zh_CN.json';
|
import frCAMessages from './messages/fr_CA.json';
|
||||||
import ititMessages from './messages/it_IT.json';
|
|
||||||
import ptptMessages from './messages/pt_PT.json';
|
|
||||||
import dedeMessages from './messages/de_DE.json';
|
|
||||||
import hiMessages from './messages/hi.json';
|
import hiMessages from './messages/hi.json';
|
||||||
import heMessages from './messages/he.json';
|
import itMessages from './messages/it.json';
|
||||||
import idMessages from './messages/id.json';
|
import ititCAMessages from './messages/it_IT.json';
|
||||||
import kokrMessages from './messages/ko_kr.json';
|
import ptMessages from './messages/pt.json';
|
||||||
import plMessages from './messages/pl.json';
|
import ptptCAMessages from './messages/pt_PT.json';
|
||||||
import ptbrMessages from './messages/pt_br.json';
|
|
||||||
import ruMessages from './messages/ru.json';
|
import ruMessages from './messages/ru.json';
|
||||||
import thMessages from './messages/th.json';
|
|
||||||
import ukMessages from './messages/uk.json';
|
import ukMessages from './messages/uk.json';
|
||||||
|
import zhcnMessages from './messages/zh_CN.json';
|
||||||
|
// no need to import en messages-- they are in the defaultMessage field
|
||||||
|
|
||||||
const messages = {
|
const appMessages = {
|
||||||
ar: arMessages,
|
ar: arMessages,
|
||||||
es: es419Messages, // Prospectus uses es language code for spanish, added `es` option and pointed to es-419 strings.
|
|
||||||
'es-419': es419Messages,
|
'es-419': es419Messages,
|
||||||
fr: frMessages,
|
fr: frMessages,
|
||||||
'zh-cn': zhcnMessages,
|
'zh-cn': zhcnMessages,
|
||||||
'it-it': ititMessages,
|
pt: ptMessages,
|
||||||
'pt-pt': ptptMessages,
|
it: itMessages,
|
||||||
'de-de': dedeMessages,
|
de: deMessages,
|
||||||
ca: caMessages,
|
|
||||||
he: heMessages,
|
|
||||||
id: idMessages,
|
|
||||||
'ko-kr': kokrMessages,
|
|
||||||
pl: plMessages,
|
|
||||||
'pt-br': ptbrMessages,
|
|
||||||
ru: ruMessages,
|
|
||||||
th: thMessages,
|
|
||||||
uk: ukMessages,
|
|
||||||
hi: hiMessages,
|
hi: hiMessages,
|
||||||
|
'fr-ca': frCAMessages,
|
||||||
|
ru: ruMessages,
|
||||||
|
uk: ukMessages,
|
||||||
|
'de-de': dedeCAMessages,
|
||||||
|
'it-it': ititCAMessages,
|
||||||
|
'pt-pt': ptptCAMessages,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default messages;
|
export default [
|
||||||
|
paragonMessages,
|
||||||
|
appMessages,
|
||||||
|
];
|
||||||
|
|||||||
@@ -1,221 +1,168 @@
|
|||||||
{
|
{
|
||||||
"top.discount.message.15.off": "off",
|
"start.learning": "ابدأ التعلم ",
|
||||||
"top.discount.message.body": "Get {discount} your first verified certificate* with code",
|
"with.site.name": "مع {siteName}",
|
||||||
"start.learning": "Start learning",
|
"complete.your.profile.1": "أكمل",
|
||||||
"with.site.name": "with {siteName}",
|
"complete.your.profile.2": "ملفك الشخصي",
|
||||||
"code.copied": "Code copied",
|
"welcome.to.platform": "أهلا بك {username} في {siteName}",
|
||||||
"complete.your.profile.1": "Complete",
|
"institution.login.page.sub.heading": "اختر مؤسستك من القائمة أدناه",
|
||||||
"complete.your.profile.2": "your profile",
|
"logistration.sign.in": "تسجيل الدخول",
|
||||||
"welcome.to.platform": "Welcome to {siteName}, {username}!",
|
"logistration.register": "التسجيل",
|
||||||
"side.discount.message.15.off": "off",
|
"enterprisetpa.title.heading": "هل ترغب في تسجيل الدخول باستخدام بيانات {providerName} الخاصة بك؟",
|
||||||
"certificate.message": "certificate* with code",
|
"enterprisetpa.login.button.text": "أرِني وسائل أخرى لتسجيل الدخول أو للتسجيل",
|
||||||
"side.discount.message.body": "Get {discountText} your first verified {lineBreak} {certificateMsg}",
|
"enterprisetpa.login.button.text.public.account.creation.disabled": "أرني طرقًا أخرى لتسجيل الدخول",
|
||||||
"institution.login.page.sub.heading": "Choose your institution from the list below",
|
"sso.sign.in.with": "تسجيل الدخول باستخدام {providerName}",
|
||||||
"forgot.password.confirmation.title": "Check your email",
|
"sso.create.account.using": "إنشاء حساب باستخدام {providerName}",
|
||||||
"forgot.password.confirmation.support.link": "contact technical support",
|
"show.password": "إظهار كلمة المرور",
|
||||||
"forgot.password.confirmation.info": "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.",
|
"hide.password": "اخفاء كلمة المرور",
|
||||||
"logistration.sign.in": "Sign in",
|
"one.letter": "حرف واحد",
|
||||||
"logistration.register": "Register",
|
"one.number": "رقم واحد",
|
||||||
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
"eight.characters": "8 رموز",
|
||||||
"server.ratelimit.error.message": "An error has occurred because of too many requests. Please try again after some time.",
|
"password.sr.only.helping.text": "يجب أن تحتوي كلمة المرور على الأقل 8 رموز، منها حرف واحد و رقم واحد على الأقل.",
|
||||||
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?",
|
"tpa.alert.heading": "انتهينا تقريبا!",
|
||||||
"enterprisetpa.sso.button.title": "Sign in using {providerName}",
|
"login.third.party.auth.account.not.linked": "لقد نجحت في تسجيل الدخول إلى {currentProvider}، لكن حسابك على {currentProvider} غير موصول بأي حساب على {platformName}. لوصل حساباتك، سجّل الدخول الآن باستخدام كلمة مرورك على {platformName}.",
|
||||||
"enterprisetpa.login.button.text": "Show me other ways to sign in or register",
|
"register.third.party.auth.account.not.linked": "لقد سجلت دخولك بنجاح إلى {currentProvider}! نحتاج فقط قليلاً بعدُ من المعلومات قبل أن تبدأ التعلم مع {platformName}.",
|
||||||
"sso.sign.in.with": "Sign in with {providerName}",
|
"registration.using.tpa.form.heading": "إتمام إنشاء حسابك",
|
||||||
"sso.create.account.using": "Create account using {providerName}",
|
"zendesk.supportTitle": "edX Support",
|
||||||
"show.password": "Show password",
|
"zendesk.selectTicketForm": "الرجاء اختيار نوع الطلب الخاص بك:",
|
||||||
"hide.password": "Hide password",
|
"error.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في العنوان. رجاءً تحقق من العنوان و حاول مجددًا.",
|
||||||
"one.letter": "1 Letter",
|
"forgot.password.confirmation.message": "لقد أرسلنا بريدًا إلكترونيًا إلى {email} به إرشادات لإعادة ضبط كلمة المرور الخاصة بك. إن لم تستلم رسالة إعادة ضبط كلمة المرور بعد دقيقة واحدة، فتحقق من إدخال عنوان البريد الإلكتروني الصحيح، أو تفقد مجلد الرسائل غير المرغوب فيها. إن احتجت مزيدًا من المساعدة، {supportLink}.",
|
||||||
"one.number": "1 Number",
|
"forgot.password.page.title": "نسيت كلمة المرور | {siteName}",
|
||||||
"eight.characters": "8 Characters",
|
"forgot.password.page.heading": "إعادة ضبط كلمة المرور",
|
||||||
"password.sr.only.helping.text": "Password must contain at least 8 characters, at least one letter, and at least one number",
|
"forgot.password.page.instructions": "رجاءً أدخل عنوان بريدك الإلكتروني أدناه وسنرسل إليك بريدًا به إرشادات بخصوص كيفية إعادة ضبط كلمة مرورك.",
|
||||||
"tpa.alert.heading": "Almost done!",
|
"forgot.password.page.invalid.email.message": "أدخل عنوان بريد إلكتروني صحيح",
|
||||||
"login.third.party.auth.account.not.linked": "You have successfully signed into {currentProvider}, but your {currentProvider} account does not have a linked {platformName} account. To link your accounts, sign in now using your {platformName} password.",
|
"forgot.password.page.email.field.label": "البريد الإلكتروني",
|
||||||
"register.third.party.auth.account.not.linked": "You've successfully signed into {currentProvider}! We just need a little more information before you start learning with {platformName}.",
|
"forgot.password.page.submit.button": "إرسال",
|
||||||
"error.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في نص الرابط. الرجاء التحقق من الرابط والمحاولة مجددا.",
|
"forgot.password.error.alert.title.": "لم نتمكن من الاتصال بك.",
|
||||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password.\n If you do not receive a password reset message after 1 minute, verify that you entered\n the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
"forgot.password.error.message.title": "حدث خطأ ما.",
|
||||||
"forgot.password.page.title": "Forgot Password | {siteName}",
|
"forgot.password.request.in.progress.message": "طلبك السابق قيد التنفيذ، يرجى المحاولة مرة أخرى بعد لحظات قليلة.",
|
||||||
"forgot.password.page.heading": "Reset password",
|
"forgot.password.empty.email.field.error": "أدخل بريدك الإلكتروني",
|
||||||
"forgot.password.page.instructions": "Please enter your email address below and we will send you an email with instructions on how to reset your password.",
|
"forgot.password.email.help.text": "عنوان البريد الإلكتروني الذي استخدمته للتسجيل في {platformName}",
|
||||||
"forgot.password.page.invalid.email.message": "Enter a valid email address",
|
"confirmation.message.title": "تفقّد بريدك الإلكتروني",
|
||||||
"forgot.password.page.email.field.label": "Email",
|
"confirmation.support.link": "اتصل بالدعم الفني",
|
||||||
"forgot.password.page.submit.button": "Submit",
|
"need.help.sign.in.text": "هل تحتاج مساعدة في تسجيل الدخول؟",
|
||||||
"forgot.password.error.alert.title.": "We were unable to contact you.",
|
"additional.help.text": "للمزيد من المساعدة، اتصل بدعم {platformName} على ",
|
||||||
"forgot.password.error.message.title": "An error occurred.",
|
"sign.in.text": "تسجيل الدخول",
|
||||||
"forgot.password.request.in.progress.message": "Your previous request is in progress, please try again in a few moments.",
|
"extend.field.errors": "{emailError} أدناه.",
|
||||||
"forgot.password.empty.email.field.error": "Enter your email",
|
"invalid.token.heading": "رابط إعادة ضبط كلمة المرور غير صالح",
|
||||||
"forgot.password.invalid.email": "An error occurred.",
|
"invalid.token.error.message": "رابط إعادة ضبط كلمة المرور هذا غير صالح. قد يكون مستعمَلا من قبل. أدخل بريدك الإلكتروني أدناه لتلقي رابط جديد.",
|
||||||
"forgot.password.invalid.email.message": "The email address you've provided isn't formatted correctly.",
|
"token.validation.rate.limit.error.heading": "طلبات أكثر مما ينبغي",
|
||||||
"forgot.password.email.help.text": "The email address you used to register with {platformName}",
|
"token.validation.rate.limit.error": "حدث خطأ بسبب كثرة الطلبات. رجاءً حاول مرة أخرى بعد مضي بعض الوقت.",
|
||||||
"confirmation.message.title": "Check your email",
|
"token.validation.internal.sever.error.heading": "فشل في التحقق من صحة الشارة",
|
||||||
"confirmation.support.link": "contact technical support",
|
"token.validation.internal.sever.error": "حدث خطأ ما. جرب تحديث الصفحة أو تحقق من اتصالك بالإنترنت.",
|
||||||
"need.help.sign.in.text": "Need help signing in?",
|
"internal.server.error": "حدث خطأ ما. جرب تحديث الصفحة أو تحقق من اتصالك بالإنترنت.",
|
||||||
"additional.help.text": "For additional help, contact edX support at ",
|
"account.activation.error.message": "شي ما لم يسر على ما يرام، يرجى {supportLink} لحل هذه المشكلة.",
|
||||||
"sign.in.text": "Sign in",
|
"login.inactive.user.error": "أنت بحاجة لتفعيل حسابك من أجل تسجيل الدخول{lineBreak}\n{lineBreak}لقد أرسلنا للتو رابطًا للتفعيل إلى {email}. إن لم تتلقّ بريدًا إلكترونيا، تفقّد مجلدات الرسائل غير المرغوب فيها أو {supportLink}.",
|
||||||
"extend.field.errors": "{emailError} below.",
|
"allowed.domain.login.error": "كونك مستخدمًا على {allowedDomain}، فإن عليك تسجيل الدخول باستخدام {tpaLink} الخاص بـ {allowedDomain} .",
|
||||||
"invalid.token.heading": "Invalid password reset link",
|
"login.incorrect.credentials.error.attempts.text.1": "اسم المستخدم أو البريد الإلكتروني أو كلمة المرور التي أدخلتها غير صحيحة. لديك {remainingAttempts, plural,\n one {محاولة واحدة}\n two {محاولتان}\n few {# محاولات}\n many {# محاولة}\n other {# محاولة}\n} أخرى لتسجيل الدخول قبل أن يتم إقفال حسابك مؤقتًا.",
|
||||||
"invalid.token.error.message": "This password reset link is invalid. It may have been used already. Enter your email below to receive a new link.",
|
"login.incorrect.credentials.error.attempts.text.2": "إن نسيت كلمة مرورك، {resetLink}",
|
||||||
"token.validation.rate.limit.error.heading": "Too many requests",
|
"account.locked.out.message.2": "لتكون في مأمن، يمكنك {resetLink} قبل تكرار المحاولة.",
|
||||||
"token.validation.rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
|
"login.incorrect.credentials.error.with.reset.link": "اسم المستخدم أو البريد الإلكتروني أو كلمة المرور التي أدخلتها غير صحيحة. يرجى تكرار المحاولة أو {resetLink}.",
|
||||||
"token.validation.internal.sever.error.heading": "Token validation failure",
|
"login.page.title": "تسجيل الدخول | {siteName}",
|
||||||
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
"login.user.identity.label": "اسم المستخدم أو البريد الإلكتروني",
|
||||||
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
"login.password.label": "كلمة المرور",
|
||||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
|
"sign.in.button": "تسجيل الدخول",
|
||||||
"account.activation.error.message": "Something went wrong, please {supportLink} to resolve this issue.",
|
"forgot.password": "نسيت كلمة المرور",
|
||||||
"login.inactive.user.error": "أنت بحاجة لتنشيط حسابك من أجل تسجيل الدخول {lineBreak}\n{lineBreak}لقد أرسلنا للتو رابط التفعيل إلى البريد الإلكتروني {email}. تحقق من مجلدات الرسائل غير المرغوب فيها أو {supportLink} إذا لم تستلم بريدًا إلكترونيًا.",
|
"institution.login.button": "بيانات المؤسسة / الجامعة",
|
||||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in\n attempts before your account is temporarily locked.",
|
"institution.login.page.title": "تسجيل الدخول باستخدام بيانات المؤسسة / الجامعة",
|
||||||
"login.incorrect.credentials.error.attempts.text.2": "If you've forgotten your password, {resetLink}",
|
"login.other.options.heading": "أو قم بتسجيل الدخول باستخدام:",
|
||||||
"account.locked.out.message.2": "To be on the safe side, you can {resetLink} before trying again.",
|
"non.compliant.password.title": "لقد غيرنا متطلبات أمان كلمة المرور مؤخرًا",
|
||||||
"login.incorrect.credentials.error.with.reset.link": "The username, email, or password you entered is incorrect. Please try again or {resetLink}.",
|
"non.compliant.password.message": "كلمة مرورك الحالية لا تستسجيب لمتطلبات الأمان الجديدة. لقد أرسلنا للتو رسالة لإعادة ضبط كلمة المرور إلى عنوان البريد الإلكتروني المرتبط بهذا الحساب. شكرًا لك على مساعدتنا في الحفاظ على سلامة بياناتك.",
|
||||||
"login.page.title": "Login | {siteName}",
|
"account.locked.out.message.1": "لحماية حسابك، تم إقفاله مؤقتًا. حاول مرة أخرى بعد 30 دقيقة.",
|
||||||
"login.user.identity.label": "Username or email",
|
"enterprise.login.btn.text": "بيانات الشركة أو المدرسة",
|
||||||
"login.password.label": "Password",
|
"username.or.email.format.validation.less.chars.message": "يجب أن يحتوي اسم المستخدم أو البريد الإلكتروني على 3 أحرف على الأقل.",
|
||||||
"sign.in.button": "Sign in",
|
"email.validation.message": "أدخل اسم المستخدم أو البريد الإلكتروني الخاص بك",
|
||||||
"sign.in.btn.pending.state": "Loading",
|
"password.validation.message": "لم يتم استيفاء معايير كلمة المرور",
|
||||||
"need.help.signing.in.collapsible.menu": "Need help signing in?",
|
"account.activation.success.message.title": "نجح الأمر! لقد قمت بتفعيل حسابك.",
|
||||||
"forgot.password.link": "Forgot my password",
|
"account.activation.success.message": "ستصلك الآن تحديثات وتنبيهات عبر البريد الإلكتروني منا تتعلق بالمساقات التي قمت بالتسجيل فيها. قم بتسجيل الدخول للمتابعة.",
|
||||||
"forgot.password": "Forgot password",
|
"account.activation.info.message": "هذا الحساب مفعَّل من قبل.",
|
||||||
"other.sign.in.issues": "Other sign in issues",
|
"account.activation.error.message.title": "لا يمكن تفعيل حسابك",
|
||||||
"need.other.help.signing.in.collapsible.menu": "Need other help signing in?",
|
"account.activation.support.link": "الاتصال بالدعم",
|
||||||
"institution.login.button": "Institution/campus credentials",
|
"account.confirmation.success.message.title": "نجحت العملية! لقد أكدت بريدك الإلكتروني.",
|
||||||
"institution.login.page.title": "Sign in with institution/campus credentials",
|
"account.confirmation.success.message": "سجل دخولك للمتابعة.",
|
||||||
"institution.login.page.back.button": "Back to sign in",
|
"account.confirmation.info.message": "هذا البريد الإلكتروني مؤكد من قبل.",
|
||||||
"create.an.account": "Create an account",
|
"account.confirmation.error.message.title": "لا يمكن تأكيد بريدك الإلكتروني",
|
||||||
"login.other.options.heading": "Or sign in with:",
|
"tpa.account.link": "حساب {provider}",
|
||||||
"non.compliant.password.title": "We recently changed our password requirements",
|
"internal.server.error.message": "حدث خطأ ما. جرّب تحديث الصفحة أو تحقق من اتصالك بالانترنت.",
|
||||||
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
|
"login.rate.limit.reached.message": "كثرت محاولات تسجيل الدخول الفاشلة. رجاءً أعد المحاولة لاحقًا.",
|
||||||
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
|
"login.failure.header.title": "لم نتمكّن من تسجيل دخولك.",
|
||||||
"first.time.here": "First time here?",
|
"contact.support.link": "اتصل بدعم {platformName}",
|
||||||
"email.help.message": "The email address you used to register with edX.",
|
"login.incorrect.credentials.error": "اسم المستخدم أو البريد الإلكتروني أو كلمة المرور التي أدخلتها غير صحيحة. حاول مرة اخرى.",
|
||||||
"enterprise.login.btn.text": "Company or school credentials",
|
"login.form.invalid.error.message": "رجاءً املأ الحقول أدناه.",
|
||||||
"email.format.validation.message": "The email address you've provided isn't formatted correctly.",
|
"login.incorrect.credentials.error.reset.link.text": "إعادة ضبط كلمه المرور",
|
||||||
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
|
"login.incorrect.credentials.error.before.account.blocked.text": "انقر هنا لإعادة ضبطها.",
|
||||||
"email.validation.message": "Enter your username or email",
|
"password.security.nudge.title": "أمان كلمة المرور",
|
||||||
"password.validation.message": "Password criteria has not been met",
|
"password.security.block.title": "مطلوب تغيير كلمة المرور",
|
||||||
"register.link": "Create an account",
|
"password.security.nudge.body": "اكتشف نظامنا أن كلمة مرورك ضعيفة. ننصحك بتغييرها حتى يظل حسابك آمنًا.",
|
||||||
"sign.in.heading": "Sign in",
|
"password.security.block.body": "اكتشف نظامنا أن كلمة مرورك صعيفة. غيّر كلمة مرورك حتى يظل حسابك آمنًا.",
|
||||||
"account.activation.success.message.title": "Success! You have activated your account.",
|
"password.security.close.button": "إغلاق",
|
||||||
"account.activation.success.message": "You will now receive email updates and alerts from us related to the courses you are enrolled in. Sign in to continue.",
|
"password.security.redirect.to.reset.password.button": "إعادة ضبط كلمة المرور",
|
||||||
"account.activation.info.message": "This account has already been activated.",
|
"login.tpa.authentication.failure": "عذرًا ، غير مصرح لك بالوصول إلى {platform_name} عبر هذه القناة. يرجى الاتصال بمسؤول التعلم أو المدير من أجل الوصول إلى {platform_name}. {lineBreak} {lineBreak} تفاصيل الخطأ: {lineBreak} {errorMessage}",
|
||||||
"account.activation.error.message.title": "Your account could not be activated",
|
"progressive.profiling.page.title": "مرحبا بكم | {siteName}",
|
||||||
"account.activation.support.link": "contact support",
|
"progressive.profiling.page.heading": "بعض الأسئلة الموجهة لك ستساعدنا كي نزداد ذكاءً.",
|
||||||
"account.confirmation.success.message.title": "Success! You have confirmed your email.",
|
"optional.fields.information.link": "معرفة المزيد عن كيفية استخدامنا لهذه المعلومات.",
|
||||||
"account.confirmation.success.message": "Sign in to continue.",
|
"optional.fields.submit.button": "إرسال",
|
||||||
"account.confirmation.info.message": "This email has already been confirmed.",
|
"optional.fields.skip.button": "التخطي مؤقتا",
|
||||||
"account.confirmation.error.message.title": "Your email could not be confirmed",
|
"optional.fields.next.button": "التالي",
|
||||||
"login.rate.limit.reached.message": "Too many failed login attempts. Try again later.",
|
"continue.to.platform": "المواصلة إلى {platformName}",
|
||||||
"login.failure.header.title": "We couldn't sign you in.",
|
"modal.title": "شكرا لإعلامنا.",
|
||||||
"contact.support.link": "contact {platformName} support",
|
"modal.description": "إن غيرت رأيك، قيمكنك إكمال ملفك الشخصي ضمن الإعدادات في أي وقت.",
|
||||||
"login.incorrect.credentials.error": "The username, email, or password you entered is incorrect. Please try again.",
|
"welcome.page.error.heading": "لم نتمكن من تحديث ملفك الشخصي",
|
||||||
"login.failed.attempt.error": "You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
"welcome.page.error.message": "حدث خطأ ما. يمكنك إكمال ملفك الشخصي ضمن الإعدادات في أي وقت.",
|
||||||
"login.locked.out.error.message": "To protect your account, it’s been temporarily locked. Try again in {lockedOutPeriod} minutes.",
|
"recommendation.page.title": "التوصيات | {siteName}",
|
||||||
"login.form.invalid.error.message": "Please fill in the fields below.",
|
"recommendation.page.heading": "لدينا بعض التوصيات لكي تبدأ.",
|
||||||
"login.incorrect.credentials.error.reset.link.text": "reset your password",
|
"recommendation.skip.button": "التخطي مؤقتا",
|
||||||
"login.incorrect.credentials.error.before.account.blocked.text": "click here to reset it.",
|
"register.page.title": "التسجيل | {siteName}",
|
||||||
"password.security.nudge.title": "Password security",
|
"registration.fullname.label": "الاسم الكامل",
|
||||||
"password.security.block.title": "Password change required",
|
"registration.email.label": "البريد الإلكتروني",
|
||||||
"password.security.nudge.body": "Our system detected that your password is vulnerable. We recommend you change it so that your account stays secure.",
|
"registration.username.label": "اسم المستخدم العامّ",
|
||||||
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
|
"registration.password.label": "كلمة المرور",
|
||||||
"password.security.close.button": "Close",
|
"registration.country.label": "البلد / المنطقة",
|
||||||
"password.security.redirect.to.reset.password.button": "Reset your password",
|
"registration.opt.in.label": "أوافق على تلقّي رسائل تسويقية من {siteName}.",
|
||||||
"register.page.title": "Register | {siteName}",
|
"help.text.name": "سيتم استخدام هذا الاسم في أي شهادات تحصل عليها.",
|
||||||
"registration.fullname.label": "Full name",
|
"help.text.username.1": "الاسم الذي ستُعرَف به في مساقاتك.",
|
||||||
"registration.email.label": "Email",
|
"help.text.username.2": "لا يمكن تغيير هذا لاحقًا.",
|
||||||
"registration.username.label": "Public username",
|
"help.text.email": "لتفعيل الحساب و التحديثات الهامة",
|
||||||
"registration.password.label": "Password",
|
"create.account.for.free.button": "إنشاء حساب مجانا",
|
||||||
"registration.country.label": "Country/Region",
|
"registration.other.options.heading": "أو سجل باستخدام:",
|
||||||
"registration.opt.in.label": "I agree that {siteName} may send me marketing messages.",
|
"register.institution.login.button": "بيانات المؤسسة / الجامعة",
|
||||||
"help.text.name": "This name will be used by any certificates that you earn.",
|
"register.institution.login.page.title": "التسجيل باستخدام بيانات المؤسسة / الجامعة",
|
||||||
"help.text.username.1": "The name that will identify you in your courses.",
|
"empty.name.field.error": "أدخل اسمك الكامل",
|
||||||
"help.text.username.2": "This can not be changed later.",
|
"empty.email.field.error": "أدخل بريدك الإلكتروني",
|
||||||
"help.text.email": "For account activation and important updates",
|
"empty.username.field.error": "يجب أن يتكون اسم المستخدم من 2 إلى 30 حرفًا",
|
||||||
"create.account.button": "Create an account",
|
"empty.password.field.error": "لم يتم استيفاء معايير كلمة المرور",
|
||||||
"create.account.for.free.button": "Create an account for free",
|
"empty.country.field.error": "حدد بلدك أو منطقة إقامتك",
|
||||||
"create.an.account.btn.pending.state": "Loading",
|
"email.do.not.match": "عناوين البريد الإلكتروني غير متطابقة.",
|
||||||
"registration.other.options.heading": "Or register with:",
|
"email.invalid.format.error": "أدخل بريدا إلكترونيا صحيحا",
|
||||||
"register.institution.login.button": "Institution/campus credentials",
|
"username.validation.message": "يجب أن يتكون اسم المستخدم من 2 إلى 30 حرفًا",
|
||||||
"register.institution.login.page.title": "Register with institution/campus credentials",
|
"name.validation.message": "أدخل اسمًا صحيحا",
|
||||||
"empty.name.field.error": "Enter your full name",
|
"username.format.validation.message": "يمكن أن تحتوي أسماء المستخدمين فقط على أحرف (A-Z، a-z)، و أرقام (0-9)، و أسطر سفلية (_)، و واصلات (-). لا يمكن أن تحتوي أسماء المستخدمين على مسافات",
|
||||||
"empty.email.field.error": "Enter your email",
|
"registration.request.failure.header": "لم نتمكّن من إنشاء حسابك.",
|
||||||
"empty.username.field.error": "Username must be between 2 and 30 characters",
|
"registration.empty.form.submission.error": "رجاءً تحقّق من أجوبتك و حاول مجددا.",
|
||||||
"empty.password.field.error": "Password criteria has not been met",
|
"registration.request.server.error": "حدث خطأ ما. جرب تحديث الصفحة أو تحقق من اتصالك بالإنترنت.",
|
||||||
"empty.country.field.error": "Select your country or region of residence",
|
"registration.rate.limit.error": "كثرت محاولات التسجيل الفاشلة. أعد المحاولة لاحقًا.",
|
||||||
"email.invalid.format.error": "Enter a valid email address",
|
"registration.tpa.session.expired": "نفد وقت التسجيل باستخدام {provider}.",
|
||||||
"email.ratelimit.less.chars.validation.message": "Email must have 3 characters.",
|
"registration.tpa.authentication.failure": "عذرًا، غير مصرح لك بالوصول إلى {platform_name} عبر هذه القناة. يرجى الاتصال بمسؤول التعلم أو المدير من أجل الوصول إلى {platform_name}. {lineBreak} {lineBreak} تفاصيل الخطأ: {lineBreak} {errorMessage}",
|
||||||
"username.validation.message": "Username must be between 2 and 30 characters",
|
"terms.of.service.and.honor.code": "شروط الخدمة وميثاق الشرف الأكاديمي",
|
||||||
"name.validation.message": "Enter a valid name",
|
"privacy.policy": "سياسة الخصوصية",
|
||||||
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces.",
|
"honor.code": "ميثاق الشرف الأكاديمي",
|
||||||
"support.education.research": "Support education research by providing additional information. (Optional)",
|
"terms.of.service": "شروط الخدمة",
|
||||||
"registration.request.failure.header": "We couldn't create your account.",
|
"registration.username.suggestion.label": "مقترح:",
|
||||||
"registration.empty.form.submission.error": "Please check your responses and try again.",
|
"did.you.mean.alert.text": "هل تقصد",
|
||||||
"registration.request.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
"register.page.terms.of.service.and.honor.code": "بإنشاءك حسابًا، فإنك توافق على {tosAndHonorCode} و تقر بأن {platformName} و كل عضو يعالج بياناتك الشخصية وفقًا لـ{privacyPolicy}.",
|
||||||
"registration.rate.limit.error": "Too many failed registration attempts. Try again later.",
|
"register.page.honor.code": "اوافق على شروط {platformName} {tosAndHonorCode}",
|
||||||
"registration.tpa.session.expired": "Registration using {provider} has timed out.",
|
"register.page.terms.of.service": "اوافق على {platformName} {termsOfService}",
|
||||||
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
|
"sign.in": "تسجيل الدخول",
|
||||||
"privacy.policy": "Privacy Policy",
|
"reset.password.page.title": "إعادة ضبط كلمة المرور | {siteName}",
|
||||||
"registration.year.of.birth.label": "Year of birth (optional)",
|
"reset.password": "إعادة ضبط كلمة المرور",
|
||||||
"registration.field.gender.options.label": "Gender (optional)",
|
"reset.password.page.instructions": "قم بإدخال و تأكيد كلمة مرورك.",
|
||||||
"registration.goals.label": "Tell us why you're interested in edX (optional)",
|
"new.password.label": "كلمة المرور الجديدة",
|
||||||
"registration.field.gender.options.f": "Female",
|
"confirm.password.label": "تأكيد كلمة المرور",
|
||||||
"registration.field.gender.options.m": "Male",
|
"passwords.do.not.match": "كلمتا المرور غير متطابقتين",
|
||||||
"registration.field.gender.options.o": "Other/Prefer not to say",
|
"confirm.your.password": "تأكيد كلمة مرورك",
|
||||||
"registration.field.education.levels.label": "Highest level of education completed (optional)",
|
"reset.password.failure.heading": "لم نتمكن من إعادة ضبط كلمة مرورك.",
|
||||||
"registration.field.education.levels.p": "Doctorate",
|
"reset.password.form.submission.error": "رجاءً تحقق من أجوبتك وحاول مجددًا.",
|
||||||
"registration.field.education.levels.m": "Master's or professional degree",
|
"reset.server.rate.limit.error": "طلبات أكثر مما ينبغي.",
|
||||||
"registration.field.education.levels.b": "Bachelor's degree",
|
"reset.password.success.heading": "تمت إعادة ضبط كلمة المرور.",
|
||||||
"registration.field.education.levels.a": "Associate's degree",
|
"reset.password.success": "تمت إعادة ضبط كلمة مرورك. سجل الدخول إلى حسابك.",
|
||||||
"registration.field.education.levels.hs": "Secondary/high school",
|
"rate.limit.error": "حدث خطأ بسبب كثرة الطلبات. رجاءً حاول مرة أخرى بعد مضي بعض الوقت."
|
||||||
"registration.field.education.levels.jhs": "Junior secondary/junior high/middle school",
|
|
||||||
"registration.field.education.levels.el": "Elementary/primary school",
|
|
||||||
"registration.field.education.levels.none": "No formal education",
|
|
||||||
"registration.field.education.levels.other": "Other education",
|
|
||||||
"registration.username.suggestion.label": "Suggested:",
|
|
||||||
"registration.using.tpa.form.heading": "Finish creating your account",
|
|
||||||
"did.you.mean.alert.text": "Did you mean",
|
|
||||||
"certificate.msg": "*Offer not eligible for GTx’s Analytics: Essential Tools and Methods MicroMasters Program, ColumbiaX’s Corporate Finance Professional Certificate Program, or courses or programs offered by Wharton, and NYIF.",
|
|
||||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
|
|
||||||
"sign.in": "Sign in",
|
|
||||||
"reset.password.page.title": "Reset Password | {siteName}",
|
|
||||||
"reset.password": "Reset password",
|
|
||||||
"reset.password.page.instructions": "Enter and confirm your new password.",
|
|
||||||
"new.password.label": "New password",
|
|
||||||
"confirm.password.label": "Confirm password",
|
|
||||||
"passwords.do.not.match": "Passwords do not match",
|
|
||||||
"confirm.your.password": "Confirm your password",
|
|
||||||
"forgot.password.confirmation.sign.in.link": "sign in",
|
|
||||||
"reset.password.request.forgot.password.text": "Forgot password",
|
|
||||||
"reset.password.request.invalid.token.header": "Invalid password reset link",
|
|
||||||
"reset.password.empty.new.password.field.error": "Please enter your new password.",
|
|
||||||
"reset.password.failure.heading": "We couldn't reset your password.",
|
|
||||||
"reset.password.form.submission.error": "Please check your responses and try again.",
|
|
||||||
"reset.password.request.server.error": "Failed to reset password",
|
|
||||||
"reset.password.token.validation.sever.error": "Token validation failure",
|
|
||||||
"reset.server.rate.limit.error": "Too many requests.",
|
|
||||||
"reset.password.success.heading": "Password reset complete.",
|
|
||||||
"reset.password.success": "Your password has been reset. Sign in to your account.",
|
|
||||||
"progressive.profiling.page.title": "Optional Fields | {siteName}",
|
|
||||||
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
|
|
||||||
"gender.options.label": "Gender (optional)",
|
|
||||||
"gender.options.f": "Female",
|
|
||||||
"gender.options.m": "Male",
|
|
||||||
"gender.options.o": "Other/Prefer not to say",
|
|
||||||
"education.levels.label": "Highest level of education completed (optional)",
|
|
||||||
"education.levels.p": "Doctorate",
|
|
||||||
"education.levels.m": "Master's or professional degree",
|
|
||||||
"education.levels.b": "Bachelor's degree",
|
|
||||||
"education.levels.a": "Associate's degree",
|
|
||||||
"education.levels.hs": "Secondary/high school",
|
|
||||||
"education.levels.jhs": "Junior secondary/junior high/middle school",
|
|
||||||
"education.levels.el": "Elementary/primary school",
|
|
||||||
"education.levels.none": "No formal education",
|
|
||||||
"education.levels.other": "Other education",
|
|
||||||
"year.of.birth.label": "Year of birth (optional)",
|
|
||||||
"optional.fields.information.link": "Learn more about how we use this information.",
|
|
||||||
"optional.fields.submit.button": "Submit",
|
|
||||||
"optional.fields.skip.button": "Skip for now",
|
|
||||||
"continue.to.platform": "Continue to {platformName}",
|
|
||||||
"modal.title": "Thanks for letting us know.",
|
|
||||||
"modal.description": "You can complete your profile in settings at any time if you change your mind.",
|
|
||||||
"welcome.page.error.heading": "We couldn't update your profile",
|
|
||||||
"welcome.page.error.message": "An error occurred. You can complete your profile in settings at any time."
|
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user