Adding frontend-base (#120)

* Use the new header and footer.

Note: Because we’re not fully using frontend-base yet, the header is broken.  It’ll start working once frontend-base’s App singleton is properly initialized.

* Initializing the app via App.initialize

- Removes App component
- Cleans up environment configuration - SUPPORT_URL is the only custom env variable.
- Cleans up usage of SUPPORT_URL and LOGOUT_URL to take advantage of App.config.

* Convert delete-account service to use App.

* Using App for services and cleaning up associated code.

Also pulling out the frontend-auth shim, since it was also dead and was sorta API-service like.

* Cleaning up “common” and some dead code.

- Most of it goes into account-settings for now.
- Shuffling the “utils” files around to classify them better.
- Removing unused assets

* Moving files into data subdirectory in account-settings

Including all the utils stuff.

* Moving top level reducers/sagas into a data dir

* Fix import bug with sagaUtils

* Removing connected-react-router

* Ceasing to use authentication and configuration from redux

Also removing some unnecessary test config.

* Updating redux init to default to prod.

Also fixing a bug where it wasn’t going into prod mode at all.

* Moving the duplicateTpaProvider logic out of redux

This lets us stop setting initial state on redux.

Also removing url-polyfill.

* A little cleanup.

* Remove default exports to keep the pattern the same.
This commit is contained in:
David Joy
2019-10-25 12:45:37 -04:00
committed by GitHub
parent 569099e88a
commit 8020ff58b8
71 changed files with 516 additions and 1213 deletions

19
.env
View File

@@ -1,4 +1,3 @@
NODE_ENV=null
ACCESS_TOKEN_COOKIE_NAME=null
BASE_URL=null
CREDENTIALS_BASE_URL=null
@@ -9,24 +8,10 @@ LMS_BASE_URL=null
LOGIN_URL=null
LOGOUT_URL=null
MARKETING_SITE_BASE_URL=null
NODE_ENV=null
ORDER_HISTORY_URL=null
REFRESH_ACCESS_TOKEN_ENDPOINT=null
SEGMENT_KEY=null
SITE_NAME=null
USER_INFO_COOKIE_NAME=null
APPLE_APP_STORE_URL=null
CONTACT_URL=null
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=null
ENTERPRISE_MARKETING_URL=null
ENTERPRISE_MARKETING_UTM_CAMPAIGN=null
ENTERPRISE_MARKETING_UTM_SOURCE=null
FACEBOOK_URL=null
GOOGLE_PLAY_URL=null
LINKED_IN_URL=null
OPEN_SOURCE_URL=null
PRIVACY_POLICY_URL=null
REDDIT_URL=null
SUPPORT_URL=null
TERMS_OF_SERVICE_URL=null
TWITTER_URL=null
YOU_TUBE_URL=null
USER_INFO_COOKIE_NAME=null

View File

@@ -1,5 +1,3 @@
NODE_ENV='development'
PORT=1997
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='localhost:1997'
CREDENTIALS_BASE_URL='http://localhost:18150'
@@ -10,24 +8,11 @@ LMS_BASE_URL='http://localhost:18000'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/login'
MARKETING_SITE_BASE_URL='http://localhost:18000'
NODE_ENV='development'
ORDER_HISTORY_URL='localhost:1996/orders'
PORT=1997
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=null
SITE_NAME='edX'
USER_INFO_COOKIE_NAME='edx-user-info'
APPLE_APP_STORE_URL='https://www.apple.com/ios/app-store/'
CONTACT_URL='http://localhost:18000/contact'
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
ENTERPRISE_MARKETING_URL='http://example.com'
ENTERPRISE_MARKETING_UTM_CAMPAIGN='my_campaign'
ENTERPRISE_MARKETING_UTM_SOURCE='edX profile'
FACEBOOK_URL='https://www.facebook.com'
GOOGLE_PLAY_URL='https://play.google.com/store'
LINKED_IN_URL='https://www.linkedin.com'
OPEN_SOURCE_URL='http://localhost:18000/openedx'
PRIVACY_POLICY_URL='http://localhost:18000/privacy-policy'
REDDIT_URL='https://www.reddit.com'
SUPPORT_URL='http://localhost:18000/support'
TERMS_OF_SERVICE_URL='http://localhost:18000/terms-of-service'
TWITTER_URL='https://twitter.com'
YOU_TUBE_URL='https://www.youtube.com'
USER_INFO_COOKIE_NAME='edx-user-info'

View File

@@ -8,8 +8,10 @@ LMS_BASE_URL='http://localhost:18000'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/login'
MARKETING_SITE_BASE_URL='http://localhost:18000'
NODE_ENV=null
ORDER_HISTORY_URL='localhost:1996/orders'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=null
SITE_NAME='edX'
SUPPORT_URL='http://localhost:18000/support'
USER_INFO_COOKIE_NAME='edx-user-info'

237
package-lock.json generated
View File

@@ -979,9 +979,9 @@
"integrity": "sha512-APBpZvdQrC1MJWMzk33V7FR2RhBRtnH2QPLqZzS+qia7PixwgWNlnX7UfHjhx+YWkM53GdsZKs40EBkSwADuMA=="
},
"@edx/frontend-analytics": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-analytics/-/frontend-analytics-2.0.0.tgz",
"integrity": "sha512-dj01bUVFA0sIlxtMH1YhQrVjWdmmiUeDIR3QqSXAxU2vwX2F4bsYUm8t0kCyg+8CjfkTcuZvyHHfwvQwlFOx2g==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-analytics/-/frontend-analytics-3.0.0.tgz",
"integrity": "sha512-+/4ILACcNBkscq4r+ZJvx/hsUqZu9c3Sqb4nm2TyAGqEKYSw010pE6Y4d6wi4tkXW6R6wc3VA2BnXdw5g7gsrA==",
"requires": {
"form-urlencoded": "^3.0.0",
"lodash.snakecase": "^4.1.1"
@@ -995,12 +995,12 @@
}
},
"@edx/frontend-auth": {
"version": "5.3.5",
"resolved": "https://registry.npmjs.org/@edx/frontend-auth/-/frontend-auth-5.3.5.tgz",
"integrity": "sha512-zmd0yMQ5ex6bhRkA7FLbCgYfZG/F+NPScB85drtCp5cd2GLgjASYGH35s0GpCwAo7p4KcXFQ0uHmPONZro6qDA==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-auth/-/frontend-auth-7.0.1.tgz",
"integrity": "sha512-oElicVn8fbSAPEc79ivObiYgRTv62aNXP8y+utGBHQCNDi6N7bA4hXcKLjvz25hExmlzJdIR4v5IOLZiVMB5VQ==",
"requires": {
"@edx/frontend-logging": "^2.0.1",
"axios": "^0.18.0",
"axios": "^0.18.1",
"camelcase-keys": "^5.0.0",
"jwt-decode": "^2.2.0",
"pubsub-js": "^1.7.0",
@@ -1009,6 +1009,11 @@
"url-parse": "^1.4.3"
},
"dependencies": {
"@edx/frontend-logging": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-logging/-/frontend-logging-2.1.0.tgz",
"integrity": "sha512-IN0Bgh0/1Ax3TMPfZztqzdJchW4B5Px9PT4V9uu6TMj2Cj8el1CV3jrSA4Idg8C3CAkFZ/EHjmaFVCxgJ9aXVA=="
},
"universal-cookie": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-3.1.0.tgz",
@@ -1022,6 +1027,46 @@
}
}
},
"@edx/frontend-base": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-base/-/frontend-base-4.1.0.tgz",
"integrity": "sha512-jGHCsQ4um6sWrh3L0ZJm+tBmmtGWefS1ULR9PNi6sDrXgKa1gceBg2W/uYzez0glMtS7HRn5zJ1kRpJHbDW7aA==",
"requires": {
"babel-polyfill": "6.26.0",
"history": "4.9.0",
"lodash.memoize": "4.1.2",
"lodash.merge": "4.6.2",
"pubsub-js": "1.7.0",
"redux-devtools-extension": "2.13.8",
"redux-logger": "3.0.6",
"redux-thunk": "2.3.0"
},
"dependencies": {
"history": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/history/-/history-4.9.0.tgz",
"integrity": "sha512-H2DkjCjXf0Op9OAr6nJ56fcRkTSNrUiv41vNJ6IswJjif6wlpZK0BTfFbi7qK9dXLSYZxkq5lBsj3vUjlYBYZA==",
"requires": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^2.2.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^0.4.0"
}
},
"resolve-pathname": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz",
"integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg=="
},
"value-equal": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz",
"integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw=="
}
}
},
"@edx/frontend-build": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-1.2.2.tgz",
@@ -1079,53 +1124,79 @@
}
},
"@edx/frontend-component-footer": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-6.0.2.tgz",
"integrity": "sha512-TB9fc85uXLRbznjshMNYJcLcwOA1GgPxaRrga8qpybb/80p4fawZnBY1XsupHFs15j/Xltk6xvF+QIm26K7m7A==",
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-9.0.1.tgz",
"integrity": "sha512-lAiffuU95XDP/XrlrZd+mW70wQHQTWWD0BX7xjoy4s8hTKNyoOL1b8zAvwhlXFeHkcfNjn/Z3iNnhgKu8Y1SgA==",
"requires": {
"query-string": "^5.1.1"
}
},
"@edx/frontend-component-site-header": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-site-header/-/frontend-component-site-header-2.4.0.tgz",
"integrity": "sha512-Z1JICIJxG6kcpBck7YxEubPZyiBGS2z0SrRJXX7JcRRpKjDNDcooOQYG/SiZt5OMVVwOOeJdQDxhMtu3zT+fSQ==",
"requires": {
"react-responsive": "^6.1.1",
"react-transition-group": "^2.5.2"
"@fortawesome/fontawesome-svg-core": "1.2.17",
"@fortawesome/free-brands-svg-icons": "5.8.1",
"@fortawesome/free-regular-svg-icons": "5.8.1",
"@fortawesome/free-solid-svg-icons": "5.8.1",
"@fortawesome/react-fontawesome": "0.1.4"
},
"dependencies": {
"react-transition-group": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz",
"integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==",
"@fortawesome/fontawesome-svg-core": {
"version": "1.2.17",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.17.tgz",
"integrity": "sha512-TORMW/wIX2QyyGBd4XwHGPir4/0U18Wxf+iDBAUW3EIJ0/VC/ZMpJOiyiCe1f8g9h0PPzA7sqVtl8JtTUtm4uA==",
"requires": {
"dom-helpers": "^3.4.0",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2",
"react-lifecycles-compat": "^3.0.4"
"@fortawesome/fontawesome-common-types": "^0.2.17"
}
},
"@fortawesome/free-brands-svg-icons": {
"version": "5.8.1",
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.8.1.tgz",
"integrity": "sha512-NN5Nap2D5e7Lusa5uarAUkcaO7PMbme5wmUF8kofZzPUZR753zDg/UFffi+LLE2Mi9zRXCJEYmIRfMON9SxLPg==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.17"
}
},
"@fortawesome/free-regular-svg-icons": {
"version": "5.8.1",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.8.1.tgz",
"integrity": "sha512-U+tFjDyQpVdD0UPWoKRBVLhh0J1/q3iaWDrnxNMJKuKRmerc4d0jfiZdM2X7agOTcG7amvcllRBiWCu2FwYlMA==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.17"
}
}
}
},
"@edx/frontend-component-header": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-1.1.4.tgz",
"integrity": "sha512-6Mt2Q+VKdwI1PlhpJ8OE0/975fbPnOXgvLwkzoDIrYkd5OTP+VjEB3uoyqJtDENkH7d8XF1T09WJJCaTPQieUQ==",
"requires": {
"babel-polyfill": "6.26.0",
"react-responsive": "8.0.1",
"react-transition-group": "4.3.0"
},
"dependencies": {
"react-responsive": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-8.0.1.tgz",
"integrity": "sha512-caseFCvFFV4QW+JOl7inzDme+avoX4r7GPpQJ+04NCzIgbroV3BU0noPgHGxVCEFKm9IsgcKOBAf+6MqIUeQIg==",
"requires": {
"hyphenate-style-name": "^1.0.0",
"matchmediaquery": "^0.3.0",
"prop-types": "^15.6.1",
"shallow-equal": "^1.1.0"
}
}
}
},
"@edx/frontend-i18n": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-i18n/-/frontend-i18n-2.1.0.tgz",
"integrity": "sha512-TmaxNNAFFRWT0EkRxy7gQrnnIzJuFn+HO8HOaYoO4vWJhvvfr++G+0ysgyNLuxms9CdFB1LuB64yEGupFSqcVA==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@edx/frontend-i18n/-/frontend-i18n-3.0.3.tgz",
"integrity": "sha512-1oBPba9xd/GK0hSBPhTdNGtHA+76j8sWJxRD9gQ+GsHLHF7Z75GsX+Ls6pB+fTNi9tjqaVWQLsA9rMGcj92hqQ==",
"requires": {
"@cospired/i18n-iso-languages": "^2.0.2",
"glob": "^7.1.4",
"i18n-iso-countries": "^4.0.0",
"iso-countries-languages": "^0.2.1",
"react-intl": "^2.9.0",
"universal-cookie": "^4.0.0"
"@cospired/i18n-iso-languages": "2.0.2",
"glob": "7.1.4",
"i18n-iso-countries": "4.0.0",
"iso-countries-languages": "0.3.0",
"react-intl": "2.9.0",
"universal-cookie": "4.0.0"
},
"dependencies": {
"i18n-iso-countries": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-4.1.0.tgz",
"integrity": "sha512-ttqCFBUvVSwUCgyjjIG95lilFg/61INah89ih/znBYHrZAcD5HsFUr8CJBmEgIOPbw0jZFgAPAsYRPGQexMTeA=="
},
"universal-cookie": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.0.tgz",
@@ -1140,9 +1211,9 @@
}
},
"@edx/frontend-logging": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@edx/frontend-logging/-/frontend-logging-2.0.2.tgz",
"integrity": "sha512-DFj4CXcy5jStwkEN/sBiTFV5IB06KnTdWIfk/OYpx7EnMJCXL7b8BawW74xstyVjkGXRhQr08sgt/cY+hNGd7A=="
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-logging/-/frontend-logging-3.0.1.tgz",
"integrity": "sha512-kRDsPbTUxNfZdnC4KN5HratS/7bkCYv/gyvUnBcuPbiONXwSuriNIVAKCepldvhg1DTwLqQMXh+Qw6vo2r048A=="
},
"@edx/paragon": {
"version": "7.1.5",
@@ -1896,9 +1967,9 @@
}
},
"@types/cookie": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.2.tgz",
"integrity": "sha512-aHQA072E10/8iUQsPH7mQU/KUyQBZAGzTVRCUvnSz8mSvbrYsP4xEO2RSA0Pjltolzi0j8+8ixrm//Hr4umPzw=="
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz",
"integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow=="
},
"@types/events": {
"version": "3.0.0",
@@ -2803,9 +2874,9 @@
}
},
"is-buffer": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
"integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw=="
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
"integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A=="
},
"ms": {
"version": "2.0.0",
@@ -4479,16 +4550,6 @@
"integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==",
"dev": true
},
"connected-react-router": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/connected-react-router/-/connected-react-router-6.5.2.tgz",
"integrity": "sha512-qzsLPZCofSI80fwy+HgxtEgSGS4ndYUUZAWaw1dqaOGPLKX/FVwIOEb7q+hjHdnZ4v5pKZcNv5GG4urjujIoyA==",
"requires": {
"immutable": "^3.8.1",
"prop-types": "^15.7.2",
"seamless-immutable": "^7.1.3"
}
},
"console-browserify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
@@ -5086,7 +5147,8 @@
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"dev": true
},
"decompress": {
"version": "4.2.0",
@@ -5514,14 +5576,6 @@
"utila": "~0.4"
}
},
"dom-helpers": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
"integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
"requires": {
"@babel/runtime": "^7.1.2"
}
},
"dom-serializer": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz",
@@ -9473,6 +9527,11 @@
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz",
"integrity": "sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ=="
},
"i18n-iso-countries": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-4.0.0.tgz",
"integrity": "sha512-DL3U2/rW4EJKXqNofuB/mHfxaca/0gwoGc7mJblvvjkNadlfWzaPaQHtC0/Cabg4SCM9+zJpl80trKx3dRY9VQ=="
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -9691,11 +9750,6 @@
"integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==",
"dev": true
},
"immutable": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
"integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM="
},
"import-cwd": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
@@ -10336,9 +10390,9 @@
"dev": true
},
"iso-countries-languages": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/iso-countries-languages/-/iso-countries-languages-0.2.1.tgz",
"integrity": "sha512-MSLwTToJmw0VS/NdCvwsgt28zE5A/rGsPpdPOrepFgAanVDBUguAgGj/73NTIrLifICz2pTmJNHZNPwDXmAMTw=="
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/iso-countries-languages/-/iso-countries-languages-0.3.0.tgz",
"integrity": "sha512-DFiVhxSc9yJtk1FgBF07kafc7Np8otbDgjDsmM5xcljn8evda6izPmTrhE8b09+uoPGm/IEF1t4E/gHm03yAjw=="
},
"isobject": {
"version": "3.0.1",
@@ -11276,14 +11330,12 @@
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
"dev": true
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4="
},
"lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
},
"lodash.mergewith": {
"version": "4.6.2",
@@ -14323,6 +14375,7 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz",
"integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==",
"dev": true,
"requires": {
"decode-uri-component": "^0.2.0",
"object-assign": "^4.1.0",
@@ -14566,11 +14619,6 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
"integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA=="
},
"react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"react-proptype-conditional-require": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/react-proptype-conditional-require/-/react-proptype-conditional-require-1.0.4.tgz",
@@ -15884,11 +15932,6 @@
}
}
},
"seamless-immutable": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/seamless-immutable/-/seamless-immutable-7.1.4.tgz",
"integrity": "sha512-XiUO1QP4ki4E2PHegiGAlu6r82o5A+6tRh7IkGGTVg/h+UoeX4nFBeCGPOhb4CYjvkqsfm/TUtvOMYC1xmV30A=="
},
"seek-bzip": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz",
@@ -16138,6 +16181,11 @@
}
}
},
"shallow-equal": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.0.tgz",
"integrity": "sha512-Z21pVxR4cXsfwpMKMhCEIO1PCi5sp7KEp+CmOpBQ+E8GpHwKOw2sEzk7sgblM3d/j4z4gakoWEoPcjK0VJQogA=="
},
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@@ -18194,11 +18242,6 @@
"prepend-http": "^1.0.1"
}
},
"url-polyfill": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.7.tgz",
"integrity": "sha512-ZrAxYWCREjmMtL8gSbSiKKLZZticgihCvVBtrFbUVpyoETt8GQJeG2okMWA8XryDAaHMjJfhnc+rnhXRbI4DXA=="
},
"url-to-options": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz",

View File

@@ -29,12 +29,13 @@
"ie 11"
],
"dependencies": {
"@edx/frontend-analytics": "^2.0.0",
"@edx/frontend-auth": "^5.3.5",
"@edx/frontend-component-footer": "^6.0.2",
"@edx/frontend-component-site-header": "^2.4.0",
"@edx/frontend-i18n": "^2.1.0",
"@edx/frontend-logging": "^2.0.2",
"@edx/frontend-analytics": "^3.0.0",
"@edx/frontend-auth": "^7.0.1",
"@edx/frontend-base": "^4.1.0",
"@edx/frontend-component-footer": "^9.0.1",
"@edx/frontend-component-header": "^1.1.4",
"@edx/frontend-i18n": "^3.0.3",
"@edx/frontend-logging": "^3.0.1",
"@edx/paragon": "^7.1.5",
"@fortawesome/fontawesome-svg-core": "^1.2.18",
"@fortawesome/free-brands-svg-icons": "^5.8.2",
@@ -43,7 +44,6 @@
"@fortawesome/react-fontawesome": "^0.1.4",
"babel-polyfill": "^6.26.0",
"classnames": "^2.2.6",
"connected-react-router": "^6.5.2",
"font-awesome": "^4.7.0",
"form-urlencoded": "^4.0.1",
"formdata-polyfill": "^3.0.19",
@@ -52,6 +52,7 @@
"lodash.findindex": "^4.6.0",
"lodash.get": "^4.4.2",
"lodash.isempty": "^4.4.0",
"lodash.merge": "^4.6.2",
"lodash.omit": "^4.5.0",
"lodash.pick": "^4.4.0",
"lodash.snakecase": "^4.1.1",
@@ -72,8 +73,7 @@
"redux-saga": "^1.1.1",
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"universal-cookie": "^4.0.2",
"url-polyfill": "^1.1.7"
"universal-cookie": "^4.0.2"
},
"devDependencies": {
"@edx/frontend-build": "^1.2.2",

View File

@@ -1,8 +1,10 @@
import { AppContext, fetchUserAccount, App } from '@edx/frontend-base';
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import memoize from 'memoize-one';
import findIndex from 'lodash.findindex';
import { sendTrackingLogEvent } from '@edx/frontend-analytics';
import {
injectIntl,
intlShape,
@@ -11,11 +13,10 @@ import {
import { Hyperlink } from '@edx/paragon';
import messages from './AccountSettingsPage.messages';
import { fetchSettings, saveSettings, updateDraft } from './actions';
import { accountSettingsPageSelector } from './selectors';
import { Alert, PageLoading } from '../common';
import { fetchSettings, saveSettings, updateDraft } from './data/actions';
import { accountSettingsPageSelector } from './data/selectors';
import PageLoading from './PageLoading';
import Alert from './Alert';
import JumpNav from './JumpNav';
import DeleteAccount from './delete-account';
import EditableField from './EditableField';
@@ -27,7 +28,7 @@ import {
YEAR_OF_BIRTH_OPTIONS,
EDUCATION_LEVELS,
GENDER_OPTIONS,
} from './constants';
} from './data/constants';
import { fetchSiteLanguages } from './site-language';
class AccountSettingsPage extends React.Component {
@@ -53,11 +54,29 @@ class AccountSettingsPage extends React.Component {
value: '',
label: props.intl.formatMessage(messages['account.settings.field.country.options.empty']),
}].concat(props.countryOptions);
// If there is a "duplicate_provider" query parameter, that's the backend's
// way of telling us that the provider account the user tried to link is already linked
// to another Open edX account. We use this to display a message to that effect, and remove the
// parameter from the URL.
const duplicateTpaProvider = App.queryParams.duplicate_provider;
if (duplicateTpaProvider !== undefined) {
App.history.replace(App.history.location.pathname);
}
this.state = {
duplicateTpaProvider,
};
}
componentDidMount() {
this.props.fetchUserAccount(this.context.authenticatedUser.username);
this.props.fetchSettings();
this.props.fetchSiteLanguages();
sendTrackingLogEvent('edx.user.settings.viewed', {
page: 'account',
visibility: null,
user_id: this.context.authenticatedUser.userId,
});
}
getTimeZoneOptions = memoize((timeZoneOptions, countryTimeZoneOptions) => {
@@ -97,7 +116,7 @@ class AccountSettingsPage extends React.Component {
};
renderDuplicateTpaProviderMessage() {
if (!this.props.duplicateTpaProvider) {
if (!this.state.duplicateTpaProvider) {
return null;
}
@@ -109,7 +128,7 @@ class AccountSettingsPage extends React.Component {
defaultMessage="The {provider} account you selected is already linked to another edX account."
description="alert message informing the user that the third-party account they attempted to link is already linked to another edX account"
values={{
provider: <b>{this.props.duplicateTpaProvider}</b>,
provider: <b>{this.state.duplicateTpaProvider}</b>,
}}
/>
</Alert>
@@ -132,7 +151,7 @@ class AccountSettingsPage extends React.Component {
values={{
managerTitle: <b>{this.props.profileDataManager}</b>,
support: (
<Hyperlink destination={this.props.supportUrl} target="_blank">
<Hyperlink destination={App.config.SUPPORT_URL} target="_blank">
<FormattedMessage
id="account.settings.message.managed.settings.support"
defaultMessage="support"
@@ -366,7 +385,6 @@ class AccountSettingsPage extends React.Component {
<DeleteAccount
isVerifiedAccount={this.props.isActive}
hasLinkedTPA={hasLinkedTPA}
logoutUrl={this.props.logoutUrl}
/>
</div>
@@ -420,6 +438,8 @@ class AccountSettingsPage extends React.Component {
}
}
AccountSettingsPage.contextType = AppContext;
AccountSettingsPage.propTypes = {
intl: intlShape.isRequired,
loading: PropTypes.bool,
@@ -472,14 +492,12 @@ AccountSettingsPage.propTypes = {
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
})),
fetchUserAccount: PropTypes.func.isRequired,
fetchSiteLanguages: PropTypes.func.isRequired,
updateDraft: PropTypes.func.isRequired,
saveSettings: PropTypes.func.isRequired,
fetchSettings: PropTypes.func.isRequired,
duplicateTpaProvider: PropTypes.string,
tpaProviders: PropTypes.arrayOf(PropTypes.object),
supportUrl: PropTypes.string.isRequired,
logoutUrl: PropTypes.string.isRequired,
};
AccountSettingsPage.defaultProps = {
@@ -495,12 +513,12 @@ AccountSettingsPage.defaultProps = {
profileDataManager: null,
staticFields: [],
hiddenFields: ['secondary_email'],
duplicateTpaProvider: null,
tpaProviders: [],
isActive: true,
};
export default connect(accountSettingsPageSelector, {
fetchUserAccount,
fetchSettings,
saveSettings,
updateDraft,

View File

@@ -4,11 +4,11 @@ import { injectIntl, intlShape } from '@edx/frontend-i18n';
import { connect } from 'react-redux';
import { Button, Hyperlink } from '@edx/paragon';
import { betaLanguageBannerSelector } from './selectors';
import { betaLanguageBannerSelector } from './data/selectors';
import messages from './AccountSettingsPage.messages';
import { saveSettings } from './actions';
import { TRANSIFEX_LANGUAGE_BASE_URL } from './constants';
import { Alert } from '../common';
import { saveSettings } from './data/actions';
import { TRANSIFEX_LANGUAGE_BASE_URL } from './data/constants';
import Alert from './Alert';
class BetaLanguageBanner extends React.Component {
getSiteLanguageEntry(languageCode) {

View File

@@ -6,14 +6,14 @@ import { Button, Input, StatefulButton, ValidationFormGroup } from '@edx/paragon
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { SwitchContent } from '../common';
import SwitchContent from './SwitchContent';
import messages from './AccountSettingsPage.messages';
import {
openForm,
closeForm,
} from './actions';
import { editableFieldSelector } from './selectors';
} from './data/actions';
import { editableFieldSelector } from './data/selectors';
function EditableField(props) {

View File

@@ -6,14 +6,15 @@ import { Button, StatefulButton, Input, ValidationFormGroup } from '@edx/paragon
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faExclamationTriangle, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { Alert, SwitchContent } from '../common';
import Alert from './Alert';
import SwitchContent from './SwitchContent';
import messages from './AccountSettingsPage.messages';
import {
openForm,
closeForm,
} from './actions';
import { editableFieldSelector } from './selectors';
} from './data/actions';
import { editableFieldSelector } from './data/selectors';
function EmailField(props) {

View File

@@ -1,6 +1,4 @@
import { utils } from '../common';
const { AsyncActionType } = utils;
import { AsyncActionType } from './utils';
export const FETCH_SETTINGS = new AsyncActionType('ACCOUNT_SETTINGS', 'FETCH_SETTINGS');
export const SAVE_SETTINGS = new AsyncActionType('ACCOUNT_SETTINGS', 'SAVE_SETTINGS');

View File

@@ -9,10 +9,10 @@ import {
RESET_DRAFTS,
} from './actions';
import { reducer as deleteAccountReducer, DELETE_ACCOUNT } from './delete-account';
import { reducer as siteLanguageReducer, FETCH_SITE_LANGUAGES } from './site-language';
import { reducer as resetPasswordReducer, RESET_PASSWORD } from './reset-password';
import { reducer as thirdPartyAuthReducer, DISCONNECT_AUTH } from './third-party-auth';
import { reducer as deleteAccountReducer, DELETE_ACCOUNT } from '../delete-account';
import { reducer as siteLanguageReducer, FETCH_SITE_LANGUAGES } from '../site-language';
import { reducer as resetPasswordReducer, RESET_PASSWORD } from '../reset-password';
import { reducer as thirdPartyAuthReducer, DISCONNECT_AUTH } from '../third-party-auth';
export const defaultState = {
loading: false,

View File

@@ -1,5 +1,8 @@
import { call, put, delay, takeEvery, select, all } from 'redux-saga/effects';
import { App } from '@edx/frontend-base';
import { setLocale, handleRtl } from '@edx/frontend-i18n';
// Actions
import {
FETCH_SETTINGS,
@@ -16,29 +19,30 @@ import {
fetchTimeZones,
fetchTimeZonesSuccess,
} from './actions';
import { usernameSelector, userRolesSelector, siteLanguageSelector } from './selectors';
import { siteLanguageSelector } from './selectors';
// Sub-modules
import { saga as deleteAccountSaga } from './delete-account';
import { saga as resetPasswordSaga } from './reset-password';
import { saga as siteLanguageSaga, ApiService as SiteLanguageApiService } from './site-language';
import { saga as thirdPartyAuthSaga } from './third-party-auth';
import { saga as deleteAccountSaga } from '../delete-account';
import { saga as resetPasswordSaga } from '../reset-password';
import {
saga as siteLanguageSaga,
patchPreferences,
postSetLang,
} from '../site-language';
import { saga as thirdPartyAuthSaga } from '../third-party-auth';
// Services
import * as ApiService from './service';
import { setLocale, handleRtl } from '@edx/frontend-i18n'; // eslint-disable-line
import { getSettings, patchSettings, getTimeZones } from './service';
export function* handleFetchSettings() {
try {
yield put(fetchSettingsBegin());
const username = yield select(usernameSelector);
const userRoles = yield select(userRolesSelector);
const { username, roles: userRoles } = App.authenticatedUser;
const {
thirdPartyAuthProviders, profileDataManager, timeZones, ...values
} = yield call(
ApiService.getSettings,
getSettings,
username,
userRoles,
);
@@ -61,22 +65,22 @@ export function* handleSaveSettings(action) {
try {
yield put(saveSettingsBegin());
const username = yield select(usernameSelector);
const { username } = App.authenticatedUser;
const { commitValues, formId } = action.payload;
const commitData = { [formId]: commitValues };
let savedValues = null;
if (formId === 'siteLanguage') {
const previousSiteLanguage = yield select(siteLanguageSelector);
yield all([
call(SiteLanguageApiService.patchPreferences, username, { prefLang: commitValues }),
call(SiteLanguageApiService.postSetLang, commitValues),
call(patchPreferences, username, { prefLang: commitValues }),
call(postSetLang, commitValues),
]);
yield put(setLocale(commitValues));
yield put(savePreviousSiteLanguage(previousSiteLanguage.savedValue));
handleRtl();
savedValues = commitData;
} else {
savedValues = yield call(ApiService.patchSettings, username, commitData);
savedValues = yield call(patchSettings, username, commitData);
}
yield put(saveSettingsSuccess(savedValues, commitData));
if (savedValues.country) yield put(fetchTimeZones(savedValues.country));
@@ -93,7 +97,7 @@ export function* handleSaveSettings(action) {
}
export function* handleFetchTimeZones(action) {
const response = yield call(ApiService.getTimeZones, action.payload.country);
const response = yield call(getTimeZones, action.payload.country);
yield put(fetchTimeZonesSuccess(response, action.payload.country));
}

View File

@@ -5,20 +5,12 @@ import {
getLanguageList,
} from '@edx/frontend-i18n'; // eslint-disable-line
import { siteLanguageOptionsSelector, siteLanguageListSelector } from './site-language';
import { siteLanguageOptionsSelector, siteLanguageListSelector } from '../site-language';
export const storeName = 'accountSettings';
export const usernameSelector = state => state.authentication.username;
export const userRolesSelector = state => state.authentication.roles || [];
export const accountSettingsSelector = state => ({ ...state[storeName] });
const duplicateTpaProviderSelector = state => state.errors.duplicateTpaProvider;
const configurationSelector = state => state.configuration;
const editableFieldNameSelector = (state, props) => props.name;
const valuesSelector = createSelector(
@@ -172,8 +164,6 @@ export const accountSettingsPageSelector = createSelector(
timeZonesSelector,
countryTimeZonesSelector,
activeAccountSelector,
duplicateTpaProviderSelector,
configurationSelector,
(
accountSettings,
siteLanguageOptions,
@@ -187,8 +177,6 @@ export const accountSettingsPageSelector = createSelector(
timeZoneOptions,
countryTimeZoneOptions,
activeAccount,
duplicateTpaProvider,
configuration,
) => ({
siteLanguageOptions,
siteLanguage,
@@ -204,9 +192,6 @@ export const accountSettingsPageSelector = createSelector(
profileDataManager,
staticFields,
hiddenFields,
duplicateTpaProvider,
tpaProviders: accountSettings.thirdPartyAuth.providers,
supportUrl: configuration.SUPPORT_URL,
logoutUrl: configuration.LOGOUT_URL,
}),
);

View File

@@ -1,22 +1,10 @@
import { App } from '@edx/frontend-base';
import pick from 'lodash.pick';
import omit from 'lodash.omit';
import isEmpty from 'lodash.isempty';
import { applyConfiguration, handleRequestError, unpackFieldErrors } from '../common/serviceUtils';
import { configureService as configureDeleteAccountApiService } from './delete-account';
import { configureService as configureResetPasswordApiService } from './reset-password';
import { configureService as configureSiteLanguageApiService } from './site-language';
import { configureService as configureThirdPartyAuthApiService, getThirdPartyAuthProviders } from './third-party-auth';
let config = {
BASE_URL: null,
ACCOUNTS_API_BASE_URL: null,
PREFERENCES_API_BASE_URL: null,
ECOMMERCE_API_BASE_URL: null,
LMS_BASE_URL: null,
DELETE_ACCOUNT_URL: null,
PASSWORD_RESET_URL: null,
};
import { handleRequestError, unpackFieldErrors } from './utils';
import { getThirdPartyAuthProviders } from '../third-party-auth';
const SOCIAL_PLATFORMS = [
{ id: 'twitter', key: 'social_link_twitter' },
@@ -24,18 +12,6 @@ const SOCIAL_PLATFORMS = [
{ id: 'linkedin', key: 'social_link_linkedin' },
];
let apiClient = null;
export function configureService(newConfig, newApiClient) {
config = applyConfiguration(config, newConfig);
apiClient = newApiClient;
configureDeleteAccountApiService(config, apiClient);
configureResetPasswordApiService(config, apiClient);
configureSiteLanguageApiService(config, apiClient);
configureThirdPartyAuthApiService(config, apiClient);
}
function unpackAccountResponseData(data) {
const unpackedData = data;
@@ -90,7 +66,7 @@ function packAccountCommitData(commitData) {
}
export async function getAccount(username) {
const { data } = await apiClient.get(`${config.ACCOUNTS_API_BASE_URL}/${username}`);
const { data } = await App.apiClient.get(`${App.config.LMS_BASE_URL}/api/user/v1/accounts/${username}`);
return unpackAccountResponseData(data);
}
@@ -99,9 +75,9 @@ export async function patchAccount(username, commitValues) {
headers: { 'Content-Type': 'application/merge-patch+json' },
};
const { data } = await apiClient
const { data } = await App.apiClient
.patch(
`${config.ACCOUNTS_API_BASE_URL}/${username}`,
`${App.config.LMS_BASE_URL}/api/user/v1/accounts/${username}`,
packAccountCommitData(commitValues),
requestConfig,
)
@@ -122,23 +98,23 @@ export async function patchAccount(username, commitValues) {
}
export async function getPreferences(username) {
const { data } = await apiClient.get(`${config.PREFERENCES_API_BASE_URL}/${username}`);
const { data } = await App.apiClient.get(`${App.config.LMS_BASE_URL}/api/user/v1/preferences/${username}`);
return data;
}
export async function patchPreferences(username, commitValues) {
const requestConfig = { headers: { 'Content-Type': 'application/merge-patch+json' } };
const requestUrl = `${config.PREFERENCES_API_BASE_URL}/${username}`;
const requestUrl = `${App.config.LMS_BASE_URL}/api/user/v1/preferences/${username}`;
// Ignore the success response, the API does not currently return any data.
await apiClient.patch(requestUrl, commitValues, requestConfig).catch(handleRequestError);
await App.apiClient.patch(requestUrl, commitValues, requestConfig).catch(handleRequestError);
return commitValues;
}
export async function getTimeZones(forCountry) {
const { data } = await apiClient
.get(`${config.LMS_BASE_URL}/user_api/v1/preferences/time_zones/`, {
const { data } = await App.apiClient
.get(`${App.config.LMS_BASE_URL}/user_api/v1/preferences/time_zones/`, {
params: { country_code: forCountry },
})
.catch(handleRequestError);
@@ -153,8 +129,8 @@ export async function getProfileDataManager(username, userRoles) {
const userRoleNames = userRoles.map(role => role.split(':')[0]);
if (userRoleNames.includes('enterprise_learner')) {
const url = `${config.LMS_BASE_URL}/enterprise/api/v1/enterprise-learner/?username=${username}`;
const { data } = await apiClient.get(url).catch(handleRequestError);
const url = `${App.config.LMS_BASE_URL}/enterprise/api/v1/enterprise-learner/?username=${username}`;
const { data } = await App.apiClient.get(url).catch(handleRequestError);
if ('results' in data) {
for (let i = 0; i < data.results.length; i += 1) {
@@ -217,4 +193,3 @@ export async function patchSettings(username, commitValues) {
const combinedResults = Object.assign({}, ...results);
return combinedResults;
}

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getModuleState should throw an exception on a bad path 1`] = `"Unexpected state key uhoh given to getModuleState. Is your state path set up correctly?"`;

View File

@@ -0,0 +1,38 @@
import camelCase from 'lodash.camelcase';
import snakeCase from 'lodash.snakecase';
export function modifyObjectKeys(object, modify) {
// If the passed in object is not an object, return it.
if (
object === undefined ||
object === null ||
(typeof object !== 'object' && !Array.isArray(object))
) {
return object;
}
if (Array.isArray(object)) {
return object.map(value => modifyObjectKeys(value, modify));
}
// Otherwise, process all its keys.
const result = {};
Object.entries(object).forEach(([key, value]) => {
result[modify(key)] = modifyObjectKeys(value, modify);
});
return result;
}
export function camelCaseObject(object) {
return modifyObjectKeys(object, camelCase);
}
export function snakeCaseObject(object) {
return modifyObjectKeys(object, snakeCase);
}
export function convertKeyNames(object, nameMap) {
const transformer = key => (nameMap[key] === undefined ? key : nameMap[key]);
return modifyObjectKeys(object, transformer);
}

View File

@@ -1,12 +1,9 @@
import {
AsyncActionType,
modifyObjectKeys,
camelCaseObject,
snakeCaseObject,
convertKeyNames,
keepKeys,
getModuleState,
} from './utils';
} from './dataUtils';
describe('modifyObjectKeys', () => {
it('should use the provided modify function to change all keys in and object and its children', () => {
@@ -91,77 +88,3 @@ describe('convertKeyNames', () => {
});
});
});
describe('keepKeys', () => {
it('should keep the specified keys only', () => {
const result = keepKeys(
{
one: 123,
two: { three: 'skip me' },
four: 'five',
six: null,
8: 'sneaky',
},
[
'one',
'three',
'six',
'seven',
'8', // yup, the 8 integer will be converted to a string.
],
);
expect(result).toEqual({
one: 123,
six: null,
8: 'sneaky',
});
});
describe('AsyncActionType', () => {
it('should return well formatted action strings', () => {
const actionType = new AsyncActionType('HOUSE_CATS', 'START_THE_RACE');
expect(actionType.BASE).toBe('HOUSE_CATS__START_THE_RACE');
expect(actionType.BEGIN).toBe('HOUSE_CATS__START_THE_RACE__BEGIN');
expect(actionType.SUCCESS).toBe('HOUSE_CATS__START_THE_RACE__SUCCESS');
expect(actionType.FAILURE).toBe('HOUSE_CATS__START_THE_RACE__FAILURE');
expect(actionType.RESET).toBe('HOUSE_CATS__START_THE_RACE__RESET');
});
});
describe('getModuleState', () => {
const state = {
first: { red: { awesome: 'sauce' }, blue: { weak: 'sauce' } },
second: { other: 'data' },
};
it('should return everything if given an empty path', () => {
expect(getModuleState(state, [])).toEqual(state);
});
it('should resolve paths correctly', () => {
expect(getModuleState(
state,
['first'],
)).toEqual({ red: { awesome: 'sauce' }, blue: { weak: 'sauce' } });
expect(getModuleState(
state,
['first', 'red'],
)).toEqual({ awesome: 'sauce' });
expect(getModuleState(state, ['second'])).toEqual({ other: 'data' });
});
it('should throw an exception on a bad path', () => {
expect(() => {
getModuleState(state, ['uhoh']);
}).toThrowErrorMatchingSnapshot();
});
it('should return non-objects correctly', () => {
expect(getModuleState(state, ['first', 'red', 'awesome'])).toEqual('sauce');
});
});
});

View File

@@ -0,0 +1,12 @@
export {
camelCaseObject,
convertKeyNames,
modifyObjectKeys,
snakeCaseObject,
} from './dataUtils';
export {
AsyncActionType,
getModuleState,
} from './reduxUtils';
export { default as handleFailure } from './sagaUtils';
export { unpackFieldErrors, handleRequestError } from './serviceUtils';

View File

@@ -1,50 +1,32 @@
import camelCase from 'lodash.camelcase';
import snakeCase from 'lodash.snakecase';
export function modifyObjectKeys(object, modify) {
// If the passed in object is not an object, return it.
if (
object === undefined ||
object === null ||
(typeof object !== 'object' && !Array.isArray(object))
) {
return object;
/**
* Helper class to save time when writing out action types for asynchronous methods. Also helps
* ensure that actions are namespaced.
*/
export class AsyncActionType {
constructor(topic, name) {
this.topic = topic;
this.name = name;
}
if (Array.isArray(object)) {
return object.map(value => modifyObjectKeys(value, modify));
get BASE() {
return `${this.topic}__${this.name}`;
}
// Otherwise, process all its keys.
const result = {};
Object.entries(object).forEach(([key, value]) => {
result[modify(key)] = modifyObjectKeys(value, modify);
});
return result;
}
get BEGIN() {
return `${this.topic}__${this.name}__BEGIN`;
}
export function camelCaseObject(object) {
return modifyObjectKeys(object, camelCase);
}
get SUCCESS() {
return `${this.topic}__${this.name}__SUCCESS`;
}
export function snakeCaseObject(object) {
return modifyObjectKeys(object, snakeCase);
}
get FAILURE() {
return `${this.topic}__${this.name}__FAILURE`;
}
export function convertKeyNames(object, nameMap) {
const transformer = key => (nameMap[key] === undefined ? key : nameMap[key]);
return modifyObjectKeys(object, transformer);
}
export function keepKeys(data, whitelist) {
const result = {};
Object.keys(data).forEach((key) => {
if (whitelist.indexOf(key) > -1) {
result[key] = data[key];
}
});
return result;
get RESET() {
return `${this.topic}__${this.name}__RESET`;
}
}
/**
@@ -78,36 +60,3 @@ export function getModuleState(state, originalPath) {
}
return getModuleState(state[key], path);
}
/**
* Helper class to save time when writing out action types for asynchronous methods. Also helps
* ensure that actions are namespaced.
*
* TODO: Put somewhere common to it can be used by other MFEs.
*/
export class AsyncActionType {
constructor(topic, name) {
this.topic = topic;
this.name = name;
}
get BASE() {
return `${this.topic}__${this.name}`;
}
get BEGIN() {
return `${this.topic}__${this.name}__BEGIN`;
}
get SUCCESS() {
return `${this.topic}__${this.name}__SUCCESS`;
}
get FAILURE() {
return `${this.topic}__${this.name}__FAILURE`;
}
get RESET() {
return `${this.topic}__${this.name}__RESET`;
}
}

View File

@@ -0,0 +1,51 @@
import {
AsyncActionType,
getModuleState,
} from './reduxUtils';
describe('AsyncActionType', () => {
it('should return well formatted action strings', () => {
const actionType = new AsyncActionType('HOUSE_CATS', 'START_THE_RACE');
expect(actionType.BASE).toBe('HOUSE_CATS__START_THE_RACE');
expect(actionType.BEGIN).toBe('HOUSE_CATS__START_THE_RACE__BEGIN');
expect(actionType.SUCCESS).toBe('HOUSE_CATS__START_THE_RACE__SUCCESS');
expect(actionType.FAILURE).toBe('HOUSE_CATS__START_THE_RACE__FAILURE');
expect(actionType.RESET).toBe('HOUSE_CATS__START_THE_RACE__RESET');
});
});
describe('getModuleState', () => {
const state = {
first: { red: { awesome: 'sauce' }, blue: { weak: 'sauce' } },
second: { other: 'data' },
};
it('should return everything if given an empty path', () => {
expect(getModuleState(state, [])).toEqual(state);
});
it('should resolve paths correctly', () => {
expect(getModuleState(
state,
['first'],
)).toEqual({ red: { awesome: 'sauce' }, blue: { weak: 'sauce' } });
expect(getModuleState(
state,
['first', 'red'],
)).toEqual({ awesome: 'sauce' });
expect(getModuleState(state, ['second'])).toEqual({ other: 'data' });
});
it('should throw an exception on a bad path', () => {
expect(() => {
getModuleState(state, ['uhoh']);
}).toThrowErrorMatchingSnapshot();
});
it('should return non-objects correctly', () => {
expect(getModuleState(state, ['first', 'red', 'awesome'])).toEqual('sauce');
});
});

View File

@@ -1,6 +1,6 @@
import { put } from 'redux-saga/effects';
import { push } from 'connected-react-router';
import { logAPIErrorResponse } from '@edx/frontend-logging';
import { App } from '@edx/frontend-base';
export default function* handleFailure(error, failureAction = null, failureRedirectPath = null) {
if (error.fieldErrors && failureAction !== null) {
@@ -11,6 +11,6 @@ export default function* handleFailure(error, failureAction = null, failureRedir
yield put(failureAction(error.message));
}
if (failureRedirectPath !== null) {
yield put(push(failureRedirectPath));
App.history.push(failureRedirectPath);
}
}

View File

@@ -1,14 +1,3 @@
import pick from 'lodash.pick';
export function applyConfiguration(expected, actual) {
Object.keys(expected).forEach((key) => {
if (actual[key] === undefined) {
throw new Error(`Service configuration error: ${key} is required.`);
}
});
return pick(actual, Object.keys(expected));
}
/**
* Turns field errors of the form:
*

View File

@@ -9,10 +9,10 @@ import { Hyperlink } from '@edx/paragon';
import messages from './messages';
// Components
import { Alert } from '../../common';
import Alert from '../Alert';
const BeforeProceedingBanner = (props) => {
const { instructionMessageId, intl, supportUrl } = props;
const { instructionMessageId, intl, supportArticleUrl } = props;
return (
<Alert
@@ -25,7 +25,7 @@ const BeforeProceedingBanner = (props) => {
description="Error that appears if you are trying to delete your edX account, but something about your account needs attention first. The actionLink will be instructions, such as 'unlink your Facebook account'."
values={{
actionLink: (
<Hyperlink destination={supportUrl}>
<Hyperlink destination={supportArticleUrl}>
{intl.formatMessage(messages[instructionMessageId])}
</Hyperlink>
),
@@ -38,7 +38,7 @@ const BeforeProceedingBanner = (props) => {
BeforeProceedingBanner.propTypes = {
instructionMessageId: PropTypes.string.isRequired,
intl: intlShape.isRequired,
supportUrl: PropTypes.string.isRequired,
supportArticleUrl: PropTypes.string.isRequired,
};
export default injectIntl(BeforeProceedingBanner);

View File

@@ -7,7 +7,7 @@ import { faExclamationCircle, faExclamationTriangle } from '@fortawesome/free-so
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import messages from './messages';
import { Alert } from '../../common';
import Alert from '../Alert';
import PrintingInstructions from './PrintingInstructions';
export class ConfirmationModal extends Component {

View File

@@ -21,7 +21,6 @@ describe('ConfirmationModal', () => {
status: null,
errorType: null,
password: 'fluffy bunnies',
logoutUrl: 'http://localhost/logout',
};
});

View File

@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { App } from '@edx/frontend-base';
import { injectIntl, intlShape } from '@edx/frontend-i18n';
import { Button, Hyperlink } from '@edx/paragon';
@@ -46,7 +47,7 @@ export class DeleteAccount extends React.Component {
};
handleFinalClose = () => {
global.location = this.props.logoutUrl;
global.location = App.config.LOGOUT_URL;
};
render() {
@@ -87,14 +88,14 @@ export class DeleteAccount extends React.Component {
{isVerifiedAccount ? null : (
<BeforeProceedingBanner
instructionMessageId="account.settings.delete.account.please.activate"
supportUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-activate-my-account-"
supportArticleUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-activate-my-account-"
/>
)}
{hasLinkedTPA ? (
<BeforeProceedingBanner
instructionMessageId="account.settings.delete.account.please.unlink"
supportUrl="https://support.edx.org/hc/en-us/articles/207206067"
supportArticleUrl="https://support.edx.org/hc/en-us/articles/207206067"
/>
) : null}
@@ -123,7 +124,6 @@ DeleteAccount.propTypes = {
errorType: PropTypes.oneOf(['empty-password', 'server']),
hasLinkedTPA: PropTypes.bool,
isVerifiedAccount: PropTypes.bool,
logoutUrl: PropTypes.string.isRequired,
intl: intlShape.isRequired,
};

View File

@@ -24,7 +24,6 @@ describe('DeleteAccount', () => {
errorType: null,
hasLinkedTPA: false,
isVerifiedAccount: true,
logoutUrl: 'http://localhost/logout',
};
});

View File

@@ -1,6 +1,4 @@
import { utils } from '../../../common';
const { AsyncActionType } = utils;
import { AsyncActionType } from '../../data/utils';
export const DELETE_ACCOUNT = new AsyncActionType('ACCOUNT_SETTINGS', 'DELETE_ACCOUNT');
DELETE_ACCOUNT.CONFIRMATION = 'ACCOUNT_SETTINGS__DELETE_ACCOUNT__CONFIRMATION';

View File

@@ -1,24 +1,15 @@
import { App } from '@edx/frontend-base';
import formurlencoded from 'form-urlencoded';
import { applyConfiguration, handleRequestError } from '../../../common/serviceUtils';
let config = {
DELETE_ACCOUNT_URL: null,
};
let apiClient = null;
export function configureService(newConfig, newApiClient) {
config = applyConfiguration(config, newConfig);
apiClient = newApiClient;
}
import { handleRequestError } from '../../data/utils';
/**
* Request deletion of the user's account.
*/
// eslint-disable-next-line import/prefer-default-export
export async function postDeleteAccount(password) {
const { data } = await apiClient
const { data } = await App.apiClient
.post(
config.DELETE_ACCOUNT_URL,
`${App.config.LMS_BASE_URL}/api/user/v1/accounts/deactivate_logout/`,
formurlencoded({ password }),
{
headers: {

View File

@@ -1,5 +1,4 @@
export { default } from './DeleteAccount';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';
export { configureService } from './data/service';
export { DELETE_ACCOUNT } from './data/actions';

View File

@@ -1,13 +1,5 @@
import ConnectedAccountSettingsPage from './AccountSettingsPage';
import reducer from './reducers';
import saga from './sagas';
import { configureService } from './service';
import { storeName } from './selectors';
export {
configureService,
ConnectedAccountSettingsPage,
reducer,
saga,
storeName,
};
export { default } from './AccountSettingsPage';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';
export { storeName } from './data/selectors';
export { default as NotFoundPage } from './NotFoundPage';

View File

@@ -5,7 +5,7 @@ import { Hyperlink } from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { Alert } from '../../common';
import Alert from '../Alert';
const ConfirmationAlert = (props) => {
const { email } = props;

View File

@@ -1,6 +1,4 @@
import { utils } from '../../../common';
const { AsyncActionType } = utils;
import { AsyncActionType } from '../../data/utils';
export const RESET_PASSWORD = new AsyncActionType('ACCOUNT_SETTINGS', 'RESET_PASSWORD');

View File

@@ -1,21 +1,12 @@
import { App } from '@edx/frontend-base';
import formurlencoded from 'form-urlencoded';
import { applyConfiguration, handleRequestError } from '../../../common/serviceUtils';
let config = {
PASSWORD_RESET_URL: null,
};
let apiClient = null;
export function configureService(newConfig, newApiClient) {
config = applyConfiguration(config, newConfig);
apiClient = newApiClient;
}
import { handleRequestError } from '../../data/utils';
// eslint-disable-next-line import/prefer-default-export
export async function postResetPassword(email) {
const { data } = await apiClient
const { data } = await App.apiClient
.post(
config.PASSWORD_RESET_URL,
`${App.config.LMS_BASE_URL}/password_reset/`,
formurlencoded({ email }),
{
headers: {

View File

@@ -2,4 +2,3 @@ export { default } from './ResetPassword';
export { default as reducer } from './data/reducers';
export { RESET_PASSWORD } from './data/actions';
export { default as saga } from './data/sagas';
export { configureService } from './data/service';

View File

@@ -1,4 +1,4 @@
import { AsyncActionType } from '../../common/utils';
import { AsyncActionType } from '../data/utils';
export const FETCH_SITE_LANGUAGES = new AsyncActionType('SITE_LANGUAGE', 'FETCH_SITE_LANGUAGES');

View File

@@ -1,16 +1,9 @@
import reducer from './reducers';
import saga from './sagas';
import { configureService, ApiService } from './service';
import { siteLanguageOptionsSelector, siteLanguageListSelector } from './selectors';
import { fetchSiteLanguages, FETCH_SITE_LANGUAGES } from './actions';
export { default as reducer } from './reducers';
export { default as saga } from './sagas';
export {
ApiService,
configureService,
fetchSiteLanguages,
FETCH_SITE_LANGUAGES,
reducer,
saga,
siteLanguageListSelector,
siteLanguageOptionsSelector,
};
getSiteLanguageList,
patchPreferences,
postSetLang,
} from './service';
export { siteLanguageOptionsSelector, siteLanguageListSelector } from './selectors';
export { fetchSiteLanguages, FETCH_SITE_LANGUAGES } from './actions';

View File

@@ -7,13 +7,13 @@ import {
FETCH_SITE_LANGUAGES,
} from './actions';
import { ApiService } from './service';
import handleFailure from '../../common/sagaUtils';
import { getSiteLanguageList } from './service';
import { handleFailure } from '../data/utils';
function* handleFetchSiteLanguages() {
try {
yield put(fetchSiteLanguagesBegin());
const siteLanguageList = yield call(ApiService.getSiteLanguageList);
const siteLanguageList = yield call(getSiteLanguageList);
yield put(fetchSiteLanguagesSuccess(siteLanguageList));
} catch (e) {
yield call(handleFailure, e, fetchSiteLanguagesFailure);

View File

@@ -1,5 +1,5 @@
import { createSelector } from 'reselect';
import { getModuleState } from '../../common/utils';
import { getModuleState } from '../data/utils';
export const storePath = ['accountSettings', 'siteLanguage'];

View File

@@ -1,48 +1,29 @@
import { App } from '@edx/frontend-base';
import siteLanguageList from './constants';
import { snakeCaseObject, convertKeyNames } from '../../common/utils';
import { applyConfiguration } from '../../common/serviceUtils';
import { snakeCaseObject, convertKeyNames } from '../data/utils';
let config = {
BASE_URL: null,
PREFERENCES_API_BASE_URL: null,
LMS_BASE_URL: null,
};
let apiClient = null;
export function configureService(newConfig, newApiClient) {
config = applyConfiguration(config, newConfig);
apiClient = newApiClient;
}
async function getSiteLanguageList() {
export async function getSiteLanguageList() {
return siteLanguageList;
}
async function patchPreferences(username, params) {
export async function patchPreferences(username, params) {
let processedParams = snakeCaseObject(params);
processedParams = convertKeyNames(processedParams, {
pref_lang: 'pref-lang',
});
await apiClient.patch(`${config.PREFERENCES_API_BASE_URL}/${username}`, processedParams, {
await App.apiClient.patch(`${App.config.LMS_BASE_URL}/api/user/v1/preferences/${username}`, processedParams, {
headers: { 'Content-Type': 'application/merge-patch+json' },
});
return params; // TODO: Once the server returns the updated preferences object, return that.
}
async function postSetLang(code) {
export async function postSetLang(code) {
const formData = new FormData();
formData.append('language', code);
await apiClient.post(`${config.LMS_BASE_URL}/i18n/setlang/`, formData, {
await App.apiClient.post(`${App.config.LMS_BASE_URL}/i18n/setlang/`, formData, {
headers: { 'X-Requested-With': 'XMLHttpRequest' },
});
}
export const ApiService = {
getSiteLanguageList,
patchPreferences,
postSetLang,
};

View File

@@ -4,7 +4,7 @@ import { connect } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-i18n';
import { Hyperlink, StatefulButton } from '@edx/paragon';
import { Alert } from '../../common';
import Alert from '../Alert';
import { disconnectAuth } from './data/actions';
class ThirdPartyAuth extends Component {

View File

@@ -1,6 +1,4 @@
import { utils } from '../../../common';
const { AsyncActionType } = utils;
import { AsyncActionType } from '../../data/utils';
export const DISCONNECT_AUTH = new AsyncActionType('ACCOUNT_SETTINGS', 'DISCONNECT_AUTH');

View File

@@ -1,29 +1,20 @@
import { applyConfiguration, handleRequestError } from '../../../common/serviceUtils';
import { App } from '@edx/frontend-base';
let config = {
LMS_BASE_URL: null,
};
let apiClient = null;
export function configureService(newConfig, newApiClient) {
config = applyConfiguration(config, newConfig);
apiClient = newApiClient;
}
import { handleRequestError } from '../../data/utils';
export async function getThirdPartyAuthProviders() {
const { data } = await apiClient
.get(`${config.LMS_BASE_URL}/api/third_party_auth/v0/providers/user_status`)
const { data } = await App.apiClient
.get(`${App.config.LMS_BASE_URL}/api/third_party_auth/v0/providers/user_status`)
.catch(handleRequestError);
return data.map(({ connect_url: connectUrl, disconnect_url: disconnectUrl, ...provider }) => ({
...provider,
connectUrl: `${config.LMS_BASE_URL}${connectUrl}`,
disconnectUrl: `${config.LMS_BASE_URL}${disconnectUrl}`,
connectUrl: `${App.config.LMS_BASE_URL}${connectUrl}`,
disconnectUrl: `${App.config.LMS_BASE_URL}${disconnectUrl}`,
}));
}
export async function postDisconnectAuth(url) {
const { data } = await apiClient.post(url).catch(handleRequestError);
const { data } = await App.apiClient.post(url).catch(handleRequestError);
return data;
}

View File

@@ -1,5 +1,5 @@
export { default } from './ThirdPartyAuth';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';
export { configureService, getThirdPartyAuthProviders } from './data/service';
export { getThirdPartyAuthProviders, postDisconnectAuth } from './data/service';
export { DISCONNECT_AUTH } from './data/actions';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1168px" height="540px" viewBox="0 0 1168 540" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 53.2 (72643) - https://sketchapp.com -->
<title>logo</title>
<desc>Created with Sketch.</desc>
<g id="logo" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<polygon id="Path" fill="#209FDA" fill-rule="nonzero" points="1166.81993 85.5 1166.81993 2.84217094e-14 953.759925 2.84217094e-14 953.759925 85.5 1002.17993 85.5 915.859925 191.98 829.459925 85.5 878.099925 85.5 878.099925 2.84217094e-14 718.919925 2.84217094e-14 718.919925 95.72 856.479925 265.26 718.919925 434.96 718.919925 452.02 784.499925 452.02 784.499925 539.64 878.099925 539.64 878.099925 452.02 823.919925 452.02 915.919925 338.52 915.939925 338.52 1008.03993 452.02 953.759925 452.02 953.759925 539.64 1166.81993 539.64 1166.81993 452.02 1126.85993 452.02 975.319925 265.26 1121.01993 85.5"></polygon>
<polygon id="Path" fill="#026BA4" fill-rule="nonzero" points="664.019925 7.10542736e-15 664.019925 85.5 710.619925 85.5 718.919925 95.72 718.919925 7.10542736e-15"></polygon>
<polygon id="Path" fill="#026BA4" fill-rule="nonzero" points="718.919925 452.02 718.919925 434.96 705.079925 452.02 664.019925 452.02 664.019925 539.64 784.499925 539.64 784.499925 452.02"></polygon>
<path d="M321.999925,411.86 L397.659925,411.86 C388.805702,433.829527 376.258024,454.122269 360.559925,471.86 C344.364089,454.216816 331.320914,433.921419 321.999925,411.86" id="Path" fill="#78212E" fill-rule="nonzero"></path>
<path d="M360.559925,189.28 C338.58337,213.190393 322.501981,241.908137 313.599925,273.14 C317.134915,280.039338 320.007771,287.25831 322.179925,294.7 L397.059925,294.7 C399.306706,287.354671 402.25356,280.242036 405.859925,273.46 C397.464721,242.277678 381.959326,213.464341 360.559925,189.28 Z M322.179925,294.7 C328.784599,317.438017 328.978396,341.558795 322.739925,364.4 L396.399925,364.4 C389.855554,341.597488 390.06397,317.386469 396.999925,294.7 L322.179925,294.7 Z M322.179925,294.7 L308.679925,294.7 C304.690779,317.752715 304.575868,341.309464 308.339925,364.4 L322.739925,364.4 C328.978396,341.558795 328.784599,317.438017 322.179925,294.7 L322.179925,294.7 Z" id="Shape" fill="#78212E" fill-rule="nonzero"></path>
<path d="M710.619925,85.5 L664.019925,85.5 L664.019925,0.02 L576.019925,0.02 L576.019925,85.5 L632.859925,85.5 L632.859925,159.2 C598.417874,134.487772 557.04992,121.286425 514.659925,121.48 C456.044663,121.405246 400.107354,146.01621 360.559925,189.28 C381.937732,213.470272 397.422343,242.283149 405.799925,273.46 C426.944121,233.500977 468.451514,208.51034 513.659925,208.52 C581.059925,208.52 632.879925,263.16 632.879925,330.52 L632.879925,331.2 C632.539925,398.28 580.879925,452.56 513.659925,452.56 C468.477451,452.593197 426.976426,427.652566 405.799925,387.74 L405.799925,387.74 C401.869213,380.340239 398.718926,372.551658 396.399925,364.5 L308.399925,364.5 C309.686934,372.450225 311.443338,380.317312 313.659925,388.06 C315.970162,396.190434 318.775397,404.171995 322.059925,411.96 L397.659925,411.96 C388.805702,433.929527 376.258024,454.222269 360.559925,471.96 C400.107354,515.22379 456.044663,539.834754 514.659925,539.76 C571.465111,540.091874 625.745998,516.316729 664.019925,474.34 L664.019925,452.04 L705.059925,452.04 L718.899925,434.96 L718.899925,95.74 L710.619925,85.5 Z M632.879925,501.9 L632.879925,539.74 L664.019925,539.74 L664.019925,474.18 C654.623775,484.469293 644.18821,493.758755 632.879925,501.9 L632.879925,501.9 Z M313.599925,273.14 C311.569597,280.231983 309.927163,287.429316 308.679925,294.7 L322.179925,294.7 C320.007771,287.25831 317.134915,280.039338 313.599925,273.14 L313.599925,273.14 Z" id="Shape" fill="#8A8C8F" fill-rule="nonzero"></path>
<path d="M410.399925,294.7 C409.199925,287.5 407.659925,280.4 405.799925,273.46 C402.19356,280.242036 399.246706,287.354671 396.999925,294.7 C390.06397,317.386469 389.855554,341.597488 396.399925,364.4 L410.719925,364.4 C414.264276,341.293291 414.156293,317.77319 410.399925,294.7 L410.399925,294.7 Z M209.059925,121.48 C107.422724,121.487508 20.5081632,194.571683 3.05992537,294.7 L91.3999254,294.7 C107.135726,243.467257 154.465065,208.503753 208.059925,208.52 C252.638644,208.335148 293.496156,233.351373 313.599925,273.14 C322.501981,241.908137 338.58337,213.190393 360.559925,189.28 C322.206855,145.880863 266.976617,121.163964 209.059925,121.48 L209.059925,121.48 Z M297.479925,411.86 C275.077969,437.877726 242.392659,452.761934 208.059925,452.58 C153.691226,452.598435 105.87164,416.63791 90.7999254,364.4 L308.339925,364.4 C304.575868,341.309464 304.690779,317.752715 308.679925,294.7 L3.05992537,294.7 C-0.902504563,317.755068 -1.01739385,341.307372 2.71992537,364.4 L2.71992537,364.4 C19.3292424,465.441984 106.661918,539.594765 209.059925,539.6 C266.986094,539.900862 322.217868,515.161403 360.559925,471.74 C344.364089,454.096816 331.320914,433.801419 321.999925,411.74 L297.479925,411.86 Z" id="Shape" fill="#B72768" fill-rule="nonzero"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`keepKeys getModuleState should throw an exception on a bad path 1`] = `"Unexpected state key uhoh given to getModuleState. Is your state path set up correctly?"`;

View File

@@ -1,11 +0,0 @@
import { fetchUserAccount as _fetchUserAccount, UserAccountApiService } from '@edx/frontend-auth';
let userAccountApiService = null;
export function configureUserAccountApiService(configuration, apiClient) {
userAccountApiService = new UserAccountApiService(apiClient, configuration.LMS_BASE_URL);
}
export function fetchUserAccount(username) {
return _fetchUserAccount(userAccountApiService, username);
}

View File

@@ -1,41 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { logAPIErrorResponse } from '@edx/frontend-logging';
import ErrorPage from './ErrorPage';
/*
Error boundary component used to log caught errors and display the error page.
*/
export default class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, info) {
logAPIErrorResponse(`${error} ${info}`);
}
render() {
if (this.state.hasError) {
return <ErrorPage />;
}
return this.props.children;
}
}
ErrorBoundary.propTypes = {
children: PropTypes.node,
};
ErrorBoundary.defaultProps = {
children: null,
};

View File

@@ -1,42 +0,0 @@
import React, { Component } from 'react';
import { FormattedMessage } from '@edx/frontend-i18n';
import { Button } from '@edx/paragon';
export default class ErrorPage extends Component {
reload() {
window.location.reload();
}
render() {
return (
<div className="container-fluid py-5 justify-content-center align-items-start text-center">
<div className="row">
<div className="col">
<p className="my-0 py-5 text-muted">
<FormattedMessage
id="unexpected.error.message.text"
defaultMessage="An unexpected error occurred. Please click the button below to return to refresh the page."
description="error message when an unexpected error occurs"
/>
</p>
</div>
</div>
<div className="row">
<div className="col">
<Button
buttonType="primary"
onClick={this.reload}
label={
<FormattedMessage
id="unexpected.error.button.text"
defaultMessage="Try Again"
description="text for button that tries to reload the app by refreshing the page"
/>
}
/>
</div>
</div>
</div>
);
}
}

View File

@@ -1,16 +0,0 @@
import * as utils from './utils';
import Alert from './components/Alert';
import PageLoading from './components/PageLoading';
import ErrorBoundary from './components/ErrorBoundary';
import SwitchContent from './components/SwitchContent';
import { configureUserAccountApiService, fetchUserAccount } from './actions';
export {
Alert,
ErrorBoundary,
PageLoading,
SwitchContent,
utils,
configureUserAccountApiService,
fetchUserAccount,
};

View File

@@ -1,259 +0,0 @@
import React, { Component } from 'react';
import { connect, Provider } from 'react-redux';
import PropTypes from 'prop-types';
import { Route, Switch } from 'react-router-dom';
import { ConnectedRouter } from 'connected-react-router';
import { sendTrackEvent } from '@edx/frontend-analytics';
import { IntlProvider, injectIntl, intlShape, getMessages } from '@edx/frontend-i18n';
import SiteHeader from '@edx/frontend-component-site-header';
import SiteFooter from '@edx/frontend-component-footer';
import {
faFacebookSquare,
faTwitterSquare,
faLinkedin,
faRedditSquare,
} from '@fortawesome/free-brands-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { ErrorBoundary, fetchUserAccount } from '../common';
import { ConnectedAccountSettingsPage } from '../account-settings';
import FooterLogo from '../assets/edx-footer.png';
import HeaderLogo from '../assets/logo.svg';
import NotFoundPage from './NotFoundPage';
import messages from './App.messages';
function PageContent({
configuration,
username,
avatar,
intl,
}) {
const mainMenu = [
{
type: 'item',
href: `${configuration.LMS_BASE_URL}/dashboard`,
content: intl.formatMessage(messages['siteheader.links.courses']),
},
{
type: 'item',
href: `${configuration.LMS_BASE_URL}/dashboard/programs`,
content: intl.formatMessage(messages['siteheader.links.programs']),
},
{
type: 'item',
href: `${configuration.MARKETING_SITE_BASE_URL}/course`,
content: intl.formatMessage(messages['siteheader.links.content.search']),
onClick: () => {
sendTrackEvent(
'edx.bi.dashboard.find_courses_button.clicked',
{ category: 'account', label: 'header' },
);
},
},
];
const userMenu = [
{
type: 'item',
href: `${configuration.LMS_BASE_URL}`,
content: intl.formatMessage(messages['siteheader.user.menu.dashboard']),
},
{
type: 'item',
href: `${configuration.LMS_BASE_URL}/u/${username}`,
content: intl.formatMessage(messages['siteheader.user.menu.profile']),
},
{
type: 'item',
href: `${configuration.LMS_BASE_URL}/account/settings`,
content: intl.formatMessage(messages['siteheader.user.menu.account.settings']),
},
{
type: 'item',
href: configuration.ORDER_HISTORY_URL,
content: intl.formatMessage(messages['siteheader.user.menu.order.history']),
},
{
type: 'item',
href: configuration.LOGOUT_URL,
content: intl.formatMessage(messages['siteheader.user.menu.logout']),
},
];
const loggedOutItems = [
{
type: 'item',
href: `${configuration.LMS_BASE_URL}/login`,
content: intl.formatMessage(messages['siteheader.user.menu.login']),
},
{
type: 'item',
href: `${configuration.LMS_BASE_URL}/register`,
content: intl.formatMessage(messages['siteheader.user.menu.register']),
},
];
const socialLinks = [
{
title: 'Facebook',
url: configuration.FACEBOOK_URL,
icon: <FontAwesomeIcon icon={faFacebookSquare} className="social-icon" size="2x" />,
screenReaderText: 'Like edX on Facebook',
},
{
title: 'Twitter',
url: configuration.TWITTER_URL,
icon: <FontAwesomeIcon icon={faTwitterSquare} className="social-icon" size="2x" />,
screenReaderText: 'Follow edX on Twitter',
},
{
title: 'LinkedIn',
url: configuration.LINKED_IN_URL,
icon: <FontAwesomeIcon icon={faLinkedin} className="social-icon" size="2x" />,
screenReaderText: 'Follow edX on LinkedIn',
},
{
title: 'Reddit',
url: configuration.REDDIT_URL,
icon: <FontAwesomeIcon icon={faRedditSquare} className="social-icon" size="2x" />,
screenReaderText: 'Subscribe to the edX subreddit',
},
];
return (
<div id="app">
<SiteHeader
logo={HeaderLogo}
loggedIn
username={username}
avatar={avatar}
logoAltText={configuration.SITE_NAME}
logoDestination={`${configuration.LMS_BASE_URL}/dashboard`}
mainMenu={mainMenu}
userMenu={userMenu}
loggedOutItems={loggedOutItems}
/>
<main>
<Switch>
<Route exact path="/" component={ConnectedAccountSettingsPage} />
<Route path="/notfound" component={NotFoundPage} />
<Route path="*" component={NotFoundPage} />
</Switch>
</main>
<SiteFooter
siteName={configuration.SITE_NAME}
siteLogo={FooterLogo}
marketingSiteBaseUrl={configuration.MARKETING_SITE_BASE_URL}
supportUrl={configuration.SUPPORT_URL}
contactUrl={configuration.CONTACT_URL}
openSourceUrl={configuration.OPEN_SOURCE_URL}
termsOfServiceUrl={configuration.TERMS_OF_SERVICE_URL}
privacyPolicyUrl={configuration.PRIVACY_POLICY_URL}
appleAppStoreUrl={configuration.APPLE_APP_STORE_URL}
googlePlayUrl={configuration.GOOGLE_PLAY_URL}
socialLinks={socialLinks}
enterpriseMarketingLink={{
url: configuration.ENTERPRISE_MARKETING_URL,
queryParams: {
utm_source: configuration.ENTERPRISE_MARKETING_UTM_SOURCE,
utm_campaign: configuration.ENTERPRISE_MARKETING_UTM_CAMPAIGN,
utm_medium: configuration.ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM,
},
}}
handleAllTrackEvents={sendTrackEvent}
/>
</div>
);
}
const IntlPageContent = injectIntl(PageContent);
class App extends Component {
componentDidMount() {
const { username } = this.props;
this.props.fetchUserAccount(username);
}
render() {
return (
<ErrorBoundary>
<IntlProvider locale={this.props.locale} messages={getMessages()}>
<Provider store={this.props.store}>
<ConnectedRouter history={this.props.history}>
<IntlPageContent
configuration={this.props.configuration}
username={this.props.username}
avatar={this.props.avatar}
/>
</ConnectedRouter>
</Provider>
</IntlProvider>
</ErrorBoundary>
);
}
}
const configurationPropTypes = {
SITE_NAME: PropTypes.string.isRequired,
LMS_BASE_URL: PropTypes.string.isRequired,
LOGOUT_URL: PropTypes.string.isRequired,
MARKETING_SITE_BASE_URL: PropTypes.string.isRequired,
SUPPORT_URL: PropTypes.string.isRequired,
CONTACT_URL: PropTypes.string.isRequired,
OPEN_SOURCE_URL: PropTypes.string.isRequired,
TERMS_OF_SERVICE_URL: PropTypes.string.isRequired,
PRIVACY_POLICY_URL: PropTypes.string.isRequired,
FACEBOOK_URL: PropTypes.string.isRequired,
TWITTER_URL: PropTypes.string.isRequired,
YOU_TUBE_URL: PropTypes.string.isRequired,
LINKED_IN_URL: PropTypes.string.isRequired,
REDDIT_URL: PropTypes.string.isRequired,
APPLE_APP_STORE_URL: PropTypes.string.isRequired,
GOOGLE_PLAY_URL: PropTypes.string.isRequired,
ORDER_HISTORY_URL: PropTypes.string.isRequired,
ENTERPRISE_MARKETING_URL: PropTypes.string.isRequired,
ENTERPRISE_MARKETING_UTM_SOURCE: PropTypes.string.isRequired,
ENTERPRISE_MARKETING_UTM_CAMPAIGN: PropTypes.string.isRequired,
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: PropTypes.string.isRequired,
};
PageContent.propTypes = {
username: PropTypes.string.isRequired,
avatar: PropTypes.string,
configuration: PropTypes.shape(configurationPropTypes).isRequired,
intl: intlShape.isRequired,
};
PageContent.defaultProps = {
avatar: null,
};
App.propTypes = {
fetchUserAccount: PropTypes.func.isRequired,
username: PropTypes.string.isRequired,
avatar: PropTypes.string,
store: PropTypes.object.isRequired, // eslint-disable-line
history: PropTypes.object.isRequired, // eslint-disable-line
locale: PropTypes.string.isRequired,
configuration: PropTypes.shape(configurationPropTypes).isRequired,
};
App.defaultProps = {
avatar: null,
};
const mapStateToProps = state => ({
username: state.authentication.username,
configuration: state.configuration,
locale: state.i18n.locale,
avatar: state.userAccount.profileImage.hasImage
? state.userAccount.profileImage.imageUrlMedium
: null,
});
export default connect(
mapStateToProps,
{
fetchUserAccount,
},
)(App);

View File

@@ -1,61 +0,0 @@
import { defineMessages } from '@edx/frontend-i18n';
const messages = defineMessages({
'siteheader.links.courses': {
id: 'siteheader.links.courses',
defaultMessage: 'Courses',
description: 'Link to the learner course dashboard',
},
'siteheader.links.programs': {
id: 'siteheader.links.programs',
defaultMessage: 'Programs',
description: 'Link to the learner program dashboard',
},
'siteheader.links.content.search': {
id: 'siteheader.links.content.search',
defaultMessage: 'Discover New',
description: 'Link to the content search page',
},
'siteheader.user.menu.dashboard': {
id: 'siteheader.user.menu.dashboard',
defaultMessage: 'Dashboard',
description: 'Link to the user dashboard',
},
'siteheader.user.menu.profile': {
id: 'siteheader.user.menu.profile',
defaultMessage: 'Profile',
description: 'Link to the user profile',
},
'siteheader.user.menu.account.settings': {
id: 'siteheader.user.menu.account.settings',
defaultMessage: 'Account',
description: 'Link to account settings',
},
'siteheader.user.menu.order.history': {
id: 'siteheader.user.menu.order.history',
defaultMessage: 'Order History',
description: 'Link to order history',
},
'siteheader.user.menu.logout': {
id: 'siteheader.user.menu.logout',
defaultMessage: 'Logout',
description: 'Logout link',
},
'siteheader.user.menu.login': {
id: 'siteheader.user.menu.login',
defaultMessage: 'Login',
description: 'Login link',
},
'siteheader.user.menu.register': {
id: 'siteheader.user.menu.register',
defaultMessage: 'Sign Up',
description: 'Link to registration',
},
'app.loading.message': {
id: 'app.loading.message',
defaultMessage: 'Loading...',
description: 'Message shown when page content is loading.',
},
});
export default messages;

View File

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

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

@@ -0,0 +1,17 @@
import { combineReducers } from 'redux';
import { userAccount } from '@edx/frontend-auth';
import { reducer as i18nReducer } from '@edx/frontend-i18n';
import {
reducer as accountSettingsReducer,
storeName as accountSettingsStoreName,
} from '../account-settings';
const createRootReducer = () =>
combineReducers({
i18n: i18nReducer,
userAccount,
[accountSettingsStoreName]: accountSettingsReducer,
});
export default createRootReducer;

View File

@@ -1,5 +1,5 @@
import { all } from 'redux-saga/effects';
import { saga as accountSettingsSaga } from './account-settings';
import { saga as accountSettingsSaga } from '../account-settings';
export default function* rootSaga() {
yield all([accountSettingsSaga()]);

View File

@@ -1,46 +0,0 @@
export const configuration = {
BASE_URL: process.env.BASE_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL,
ECOMMERCE_BASE_URL: process.env.ECOMMERCE_BASE_URL,
CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL,
LOGIN_URL: process.env.LOGIN_URL,
LOGOUT_URL: process.env.LOGOUT_URL,
CSRF_TOKEN_API_PATH: process.env.CSRF_TOKEN_API_PATH,
REFRESH_ACCESS_TOKEN_ENDPOINT: process.env.REFRESH_ACCESS_TOKEN_ENDPOINT,
SEGMENT_KEY: process.env.SEGMENT_KEY,
ACCESS_TOKEN_COOKIE_NAME: process.env.ACCESS_TOKEN_COOKIE_NAME,
USER_INFO_COOKIE_NAME: process.env.USER_INFO_COOKIE_NAME,
LANGUAGE_PREFERENCE_COOKIE_NAME: process.env.LANGUAGE_PREFERENCE_COOKIE_NAME,
SITE_NAME: process.env.SITE_NAME,
MARKETING_SITE_BASE_URL: process.env.MARKETING_SITE_BASE_URL,
SUPPORT_URL: process.env.SUPPORT_URL,
CONTACT_URL: process.env.CONTACT_URL,
OPEN_SOURCE_URL: process.env.OPEN_SOURCE_URL,
TERMS_OF_SERVICE_URL: process.env.TERMS_OF_SERVICE_URL,
PRIVACY_POLICY_URL: process.env.PRIVACY_POLICY_URL,
FACEBOOK_URL: process.env.FACEBOOK_URL,
TWITTER_URL: process.env.TWITTER_URL,
YOU_TUBE_URL: process.env.YOU_TUBE_URL,
LINKED_IN_URL: process.env.LINKED_IN_URL,
REDDIT_URL: process.env.REDDIT_URL,
APPLE_APP_STORE_URL: process.env.APPLE_APP_STORE_URL,
GOOGLE_PLAY_URL: process.env.GOOGLE_PLAY_URL,
ACCOUNT_SETTINGS_URL: `${process.env.LMS_BASE_URL}/account/settings`,
DATA_API_BASE_URL: process.env.DATA_API_BASE_URL,
SECURE_COOKIES: process.env.NODE_ENV !== 'development',
ENVIRONMENT: process.env.NODE_ENV,
ACCOUNTS_API_BASE_URL: `${process.env.LMS_BASE_URL}/api/user/v1/accounts`,
PREFERENCES_API_BASE_URL: `${process.env.LMS_BASE_URL}/api/user/v1/preferences`,
CERTIFICATES_API_BASE_URL: `${process.env.LMS_BASE_URL}/api/certificates/v0/certificates`,
VIEW_MY_RECORDS_URL: `${process.env.CREDENTIALS_BASE_URL}/records`,
ECOMMERCE_API_BASE_URL: `${process.env.ECOMMERCE_BASE_URL}/api/v2`,
ORDER_HISTORY_URL: process.env.ORDER_HISTORY_URL,
DELETE_ACCOUNT_URL: `${process.env.LMS_BASE_URL}/api/user/v1/accounts/deactivate_logout/`,
PASSWORD_RESET_URL: `${process.env.LMS_BASE_URL}/password_reset/`,
ENTERPRISE_MARKETING_URL: process.env.ENTERPRISE_MARKETING_URL,
ENTERPRISE_MARKETING_UTM_SOURCE: process.env.ENTERPRISE_MARKETING_UTM_SOURCE,
ENTERPRISE_MARKETING_UTM_CAMPAIGN: process.env.ENTERPRISE_MARKETING_UTM_CAMPAIGN,
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM: process.env.ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM,
};
export const features = {};

View File

@@ -1,99 +1,52 @@
import 'babel-polyfill';
import 'url-polyfill';
import 'formdata-polyfill';
import { App, AppProvider, APP_ERROR, APP_READY, ErrorPage } from '@edx/frontend-base';
import React from 'react';
import ReactDOM from 'react-dom';
import {
configureAnalytics,
identifyAnonymousUser,
identifyAuthenticatedUser,
initializeSegment,
sendPageEvent,
sendTrackingLogEvent,
} from '@edx/frontend-analytics';
import { configureLoggingService, NewRelicLoggingService } from '@edx/frontend-logging';
import { getAuthenticatedAPIClient } from '@edx/frontend-auth';
import { configure as configureI18n } from '@edx/frontend-i18n';
import { Route, Switch } from 'react-router-dom';
import { configuration } from './environment';
import configureStore from './store';
import { configureUserAccountApiService } from './common';
import { configureService as configureAccountSettingsApiService } from './account-settings';
import messages from './i18n';
import App from './components/App';
import Header, { messages as headerMessages } from '@edx/frontend-component-header';
import Footer, { messages as footerMessages } from '@edx/frontend-component-footer';
import configureStore from './data/configureStore';
import AccountSettingsPage, { NotFoundPage } from './account-settings';
import appMessages from './i18n';
import './index.scss';
import './assets/favicon.ico';
const apiClient = getAuthenticatedAPIClient({
appBaseUrl: configuration.BASE_URL,
authBaseUrl: configuration.LMS_BASE_URL,
loginUrl: configuration.LOGIN_URL,
logoutUrl: configuration.LOGOUT_URL,
csrfTokenApiPath: configuration.CSRF_TOKEN_API_PATH,
refreshAccessTokenEndpoint: configuration.REFRESH_ACCESS_TOKEN_ENDPOINT,
accessTokenCookieName: configuration.ACCESS_TOKEN_COOKIE_NAME,
userInfoCookieName: configuration.USER_INFO_COOKIE_NAME,
loggingService: NewRelicLoggingService,
App.subscribe(APP_READY, () => {
ReactDOM.render(
<AppProvider store={configureStore()}>
<Header />
<main>
<Switch>
<Route exact path="/" component={AccountSettingsPage} />
<Route path="/notfound" component={NotFoundPage} />
<Route path="*" component={NotFoundPage} />
</Switch>
</main>
<Footer />
</AppProvider>,
document.getElementById('root'),
);
});
/**
* We need to merge the application configuration with the authentication state
* so that we can hand it all to the redux store's initializer.
*/
function createInitialState() {
const errors = {};
const url = new URL(window.location.href);
App.subscribe(APP_ERROR, (error) => {
ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root'));
});
// Extract duplicate third-party auth provider message from query string
errors.duplicateTpaProvider = url.searchParams.get('duplicate_provider');
if (errors.duplicateTpaProvider) {
// Remove the duplicate_provider query param to avoid bookmarking.
window.history.replaceState(null, '', `${url.protocol}//${url.host}${url.pathname}`);
}
return Object.assign({}, { configuration }, apiClient.getAuthenticationState(), { errors });
}
function configure() {
configureI18n(configuration, messages);
const { store, history } = configureStore(createInitialState(), configuration.ENVIRONMENT);
configureLoggingService(NewRelicLoggingService);
configureAccountSettingsApiService(configuration, apiClient);
configureUserAccountApiService(configuration, apiClient);
initializeSegment(configuration.SEGMENT_KEY);
configureAnalytics({
loggingService: NewRelicLoggingService,
authApiClient: apiClient,
analyticsApiBaseUrl: configuration.LMS_BASE_URL,
});
return {
store,
history,
};
}
apiClient.ensurePublicOrAuthenticationAndCookies(
window.location.pathname,
(accessToken) => {
const { store, history } = configure();
ReactDOM.render(<App store={store} history={history} />, document.getElementById('root'));
if (accessToken) {
identifyAuthenticatedUser(accessToken.userId);
} else {
identifyAnonymousUser();
}
sendPageEvent();
sendTrackingLogEvent('edx.user.settings.viewed', {
page: 'account',
visibility: null,
user_id: accessToken ? accessToken.userId : null,
});
App.initialize({
messages: [
appMessages,
headerMessages,
footerMessages,
],
overrideHandlers: {
loadConfig: () => {
App.mergeConfig({
SUPPORT_URL: process.env.SUPPORT_URL,
}, 'App loadConfig override handler');
},
},
);
});

View File

@@ -4,8 +4,8 @@
$fa-font-path: "~font-awesome/fonts";
@import "~font-awesome/scss/font-awesome";
@import "~@edx/frontend-component-site-header/src/index";
@import "~@edx/frontend-component-footer/src/lib/scss/site-footer";
@import "~@edx/frontend-component-header/dist/index";
@import "~@edx/frontend-component-footer/dist/footer";
@import "./account-settings/style";

View File

@@ -1,30 +0,0 @@
import { combineReducers } from 'redux';
import { userAccount } from '@edx/frontend-auth';
import { connectRouter } from 'connected-react-router';
import { reducer as i18nReducer } from '@edx/frontend-i18n'; // eslint-disable-line
import {
reducer as accountSettingsReducer,
storeName as accountSettingsStoreName,
} from './account-settings';
const identityReducer = (state) => {
const newState = { ...state };
return newState;
};
const createRootReducer = history =>
combineReducers({
// The authentication state is added as initialState when
// creating the store in data/store.js.
authentication: identityReducer,
configuration: identityReducer,
errors: identityReducer,
i18n: i18nReducer,
userAccount,
[accountSettingsStoreName]: accountSettingsReducer,
router: connectRouter(history),
});
export default createRootReducer;

View File

@@ -4,7 +4,3 @@ import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });
// These configuration values are usually set in webpack's EnvironmentPlugin however
// Jest does not use webpack so we need to set these so for testing
process.env.LMS_BASE_URL = 'http://localhost:18000';

View File

@@ -1,29 +0,0 @@
import { applyMiddleware, createStore } from 'redux';
import createSagaMiddleware from 'redux-saga';
import thunkMiddleware from 'redux-thunk';
import { createBrowserHistory } from 'history';
import { routerMiddleware } from 'connected-react-router';
import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction';
import { createLogger } from 'redux-logger';
import createRootReducer from '../reducers';
import rootSaga from '../sagas';
export default function configureStore(initialState = {}) {
const history = createBrowserHistory();
const loggerMiddleware = createLogger({
collapsed: true,
});
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
createRootReducer(history),
initialState,
composeWithDevTools(applyMiddleware(thunkMiddleware, sagaMiddleware, routerMiddleware(history), loggerMiddleware)), // eslint-disable-line
);
sagaMiddleware.run(rootSaga);
return { store, history };
}

View File

@@ -1,24 +0,0 @@
import { applyMiddleware, createStore, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import thunkMiddleware from 'redux-thunk';
import { createBrowserHistory } from 'history';
import { routerMiddleware } from 'connected-react-router';
import createRootReducer from '../reducers';
import rootSaga from '../sagas';
export default function configureStore(initialState = {}) {
const history = createBrowserHistory();
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
createRootReducer(history),
initialState,
compose(applyMiddleware(thunkMiddleware, sagaMiddleware, routerMiddleware(history))),
);
sagaMiddleware.run(rootSaga);
return { store, history };
}

View File

@@ -1,9 +0,0 @@
import configureStoreProd from './configureStore.prod';
import configureStoreDev from './configureStore.dev';
export default function configureStore(state, env) {
if (env === 'production') {
return configureStoreProd(state);
}
return configureStoreDev(state);
}