@edx/frontend-platform 4.6.0 → 4.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/.env.development +30 -0
  2. package/.env.test +30 -0
  3. package/.eslintignore +6 -0
  4. package/.eslintrc.js +28 -0
  5. package/.github/PULL_REQUEST_TEMPLATE.md +13 -0
  6. package/.github/workflows/add-depr-ticket-to-depr-board.yml +19 -0
  7. package/.github/workflows/add-remove-label-on-comment.yml +20 -0
  8. package/.github/workflows/ci.yml +42 -0
  9. package/.github/workflows/commitlint.yml +10 -0
  10. package/.github/workflows/lockfileversion-check.yml +13 -0
  11. package/.github/workflows/manual-publish.yml +43 -0
  12. package/.github/workflows/npm-deprecate.yml +22 -0
  13. package/.github/workflows/release.yml +45 -0
  14. package/.github/workflows/self-assign-issue.yml +12 -0
  15. package/.github/workflows/update-browserslist-db.yml +12 -0
  16. package/.nvmrc +1 -0
  17. package/.releaserc +32 -0
  18. package/catalog-info.yaml +21 -0
  19. package/dist/LICENSE +661 -0
  20. package/dist/README.md +155 -0
  21. package/dist/package.json +86 -0
  22. package/docs/addTagsPlugin.js +10 -0
  23. package/docs/auth-API.md +114 -0
  24. package/docs/decisions/0001-record-architecture-decisions.rst +32 -0
  25. package/docs/decisions/0002-frontend-base-design-goals.rst +222 -0
  26. package/docs/decisions/0003-consolidation-into-frontend-platform.rst +71 -0
  27. package/docs/decisions/0004-axios-caching-implementation.rst +88 -0
  28. package/docs/decisions/0005-token-null-after-successful-refresh.rst +69 -0
  29. package/docs/decisions/0006-middleware-support-for-http-clients.rst +44 -0
  30. package/docs/decisions/0007-javascript-file-configuration.rst +143 -0
  31. package/docs/how_tos/automatic-case-conversion.rst +58 -0
  32. package/docs/how_tos/caching.rst +93 -0
  33. package/docs/how_tos/i18n.rst +305 -0
  34. package/docs/removeExport.js +24 -0
  35. package/docs/template/edx/README.md +12 -0
  36. package/docs/template/edx/publish.js +713 -0
  37. package/docs/template/edx/static/fonts/OpenSans-Bold-webfont.eot +0 -0
  38. package/docs/template/edx/static/fonts/OpenSans-Bold-webfont.svg +1830 -0
  39. package/docs/template/edx/static/fonts/OpenSans-Bold-webfont.woff +0 -0
  40. package/docs/template/edx/static/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  41. package/docs/template/edx/static/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
  42. package/docs/template/edx/static/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  43. package/docs/template/edx/static/fonts/OpenSans-Italic-webfont.eot +0 -0
  44. package/docs/template/edx/static/fonts/OpenSans-Italic-webfont.svg +1830 -0
  45. package/docs/template/edx/static/fonts/OpenSans-Italic-webfont.woff +0 -0
  46. package/docs/template/edx/static/fonts/OpenSans-Light-webfont.eot +0 -0
  47. package/docs/template/edx/static/fonts/OpenSans-Light-webfont.svg +1831 -0
  48. package/docs/template/edx/static/fonts/OpenSans-Light-webfont.woff +0 -0
  49. package/docs/template/edx/static/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  50. package/docs/template/edx/static/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
  51. package/docs/template/edx/static/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  52. package/docs/template/edx/static/fonts/OpenSans-Regular-webfont.eot +0 -0
  53. package/docs/template/edx/static/fonts/OpenSans-Regular-webfont.svg +1831 -0
  54. package/docs/template/edx/static/fonts/OpenSans-Regular-webfont.woff +0 -0
  55. package/docs/template/edx/static/scripts/linenumber.js +25 -0
  56. package/docs/template/edx/static/scripts/prettify/Apache-License-2.0.txt +202 -0
  57. package/docs/template/edx/static/scripts/prettify/lang-css.js +2 -0
  58. package/docs/template/edx/static/scripts/prettify/prettify.js +28 -0
  59. package/docs/template/edx/static/styles/jsdoc-default.css +356 -0
  60. package/docs/template/edx/static/styles/prettify-jsdoc.css +111 -0
  61. package/docs/template/edx/static/styles/prettify-tomorrow.css +132 -0
  62. package/docs/template/edx/tmpl/augments.tmpl +10 -0
  63. package/docs/template/edx/tmpl/container.tmpl +196 -0
  64. package/docs/template/edx/tmpl/details.tmpl +143 -0
  65. package/docs/template/edx/tmpl/example.tmpl +2 -0
  66. package/docs/template/edx/tmpl/examples.tmpl +13 -0
  67. package/docs/template/edx/tmpl/exceptions.tmpl +32 -0
  68. package/docs/template/edx/tmpl/layout.tmpl +39 -0
  69. package/docs/template/edx/tmpl/mainpage.tmpl +10 -0
  70. package/docs/template/edx/tmpl/members.tmpl +38 -0
  71. package/docs/template/edx/tmpl/method.tmpl +131 -0
  72. package/docs/template/edx/tmpl/modifies.tmpl +14 -0
  73. package/docs/template/edx/tmpl/params.tmpl +131 -0
  74. package/docs/template/edx/tmpl/properties.tmpl +108 -0
  75. package/docs/template/edx/tmpl/returns.tmpl +19 -0
  76. package/docs/template/edx/tmpl/source.tmpl +8 -0
  77. package/docs/template/edx/tmpl/tutorial.tmpl +19 -0
  78. package/docs/template/edx/tmpl/type.tmpl +7 -0
  79. package/env.config.js +8 -0
  80. package/jsdoc.json +36 -0
  81. package/openedx.yaml +12 -0
  82. package/package.json +6 -6
  83. package/service-interface.png +0 -0
  84. package/src/analytics/MockAnalyticsService.js +71 -0
  85. package/src/analytics/SegmentAnalyticsService.js +243 -0
  86. package/src/analytics/index.js +12 -0
  87. package/src/analytics/interface.js +142 -0
  88. package/src/auth/AxiosCsrfTokenService.js +60 -0
  89. package/src/auth/AxiosJwtAuthService.js +364 -0
  90. package/src/auth/AxiosJwtTokenService.js +134 -0
  91. package/src/auth/LocalForageCache.js +78 -0
  92. package/src/auth/MockAuthService.js +285 -0
  93. package/src/auth/index.js +19 -0
  94. package/src/auth/interceptors/createCsrfTokenProviderInterceptor.js +37 -0
  95. package/src/auth/interceptors/createJwtTokenProviderInterceptor.js +38 -0
  96. package/src/auth/interceptors/createProcessAxiosRequestErrorInterceptor.js +20 -0
  97. package/src/auth/interceptors/createRetryInterceptor.js +72 -0
  98. package/src/auth/interface.js +309 -0
  99. package/src/auth/utils.js +105 -0
  100. package/src/config.js +327 -0
  101. package/src/constants.js +66 -0
  102. package/src/i18n/countries.js +57 -0
  103. package/src/i18n/index.js +123 -0
  104. package/src/i18n/injectIntlWithShim.jsx +45 -0
  105. package/src/i18n/languages.js +60 -0
  106. package/src/i18n/lib.js +282 -0
  107. package/src/i18n/scripts/README.md +29 -0
  108. package/src/i18n/scripts/intl-imports.js +259 -0
  109. package/src/i18n/scripts/transifex-utils.js +75 -0
  110. package/src/index.js +42 -0
  111. package/src/initialize.js +357 -0
  112. package/src/logging/MockLoggingService.js +31 -0
  113. package/src/logging/NewRelicLoggingService.js +181 -0
  114. package/src/logging/index.js +9 -0
  115. package/src/logging/interface.js +110 -0
  116. package/src/pubSub.js +47 -0
  117. package/src/react/AppContext.jsx +24 -0
  118. package/src/react/AppProvider.jsx +93 -0
  119. package/src/react/AuthenticatedPageRoute.jsx +60 -0
  120. package/src/react/ErrorBoundary.jsx +44 -0
  121. package/src/react/ErrorPage.jsx +76 -0
  122. package/src/react/LoginRedirect.jsx +16 -0
  123. package/src/react/OptionalReduxProvider.jsx +28 -0
  124. package/src/react/PageRoute.jsx +31 -0
  125. package/src/react/hooks.js +50 -0
  126. package/src/react/index.js +16 -0
  127. package/src/scripts/GoogleAnalyticsLoader.js +53 -0
  128. package/src/scripts/index.js +2 -0
  129. package/src/testing/index.js +9 -0
  130. package/src/testing/initializeMockApp.js +77 -0
  131. package/src/testing/mockMessages.js +21 -0
  132. package/src/utils.js +167 -0
  133. /package/{analytics → dist/analytics}/MockAnalyticsService.js +0 -0
  134. /package/{analytics → dist/analytics}/MockAnalyticsService.js.map +0 -0
  135. /package/{analytics → dist/analytics}/SegmentAnalyticsService.js +0 -0
  136. /package/{analytics → dist/analytics}/SegmentAnalyticsService.js.map +0 -0
  137. /package/{analytics → dist/analytics}/index.js +0 -0
  138. /package/{analytics → dist/analytics}/index.js.map +0 -0
  139. /package/{analytics → dist/analytics}/interface.js +0 -0
  140. /package/{analytics → dist/analytics}/interface.js.map +0 -0
  141. /package/{auth → dist/auth}/AxiosCsrfTokenService.js +0 -0
  142. /package/{auth → dist/auth}/AxiosCsrfTokenService.js.map +0 -0
  143. /package/{auth → dist/auth}/AxiosJwtAuthService.js +0 -0
  144. /package/{auth → dist/auth}/AxiosJwtAuthService.js.map +0 -0
  145. /package/{auth → dist/auth}/AxiosJwtTokenService.js +0 -0
  146. /package/{auth → dist/auth}/AxiosJwtTokenService.js.map +0 -0
  147. /package/{auth → dist/auth}/LocalForageCache.js +0 -0
  148. /package/{auth → dist/auth}/LocalForageCache.js.map +0 -0
  149. /package/{auth → dist/auth}/MockAuthService.js +0 -0
  150. /package/{auth → dist/auth}/MockAuthService.js.map +0 -0
  151. /package/{auth → dist/auth}/index.js +0 -0
  152. /package/{auth → dist/auth}/index.js.map +0 -0
  153. /package/{auth → dist/auth}/interceptors/createCsrfTokenProviderInterceptor.js +0 -0
  154. /package/{auth → dist/auth}/interceptors/createCsrfTokenProviderInterceptor.js.map +0 -0
  155. /package/{auth → dist/auth}/interceptors/createJwtTokenProviderInterceptor.js +0 -0
  156. /package/{auth → dist/auth}/interceptors/createJwtTokenProviderInterceptor.js.map +0 -0
  157. /package/{auth → dist/auth}/interceptors/createProcessAxiosRequestErrorInterceptor.js +0 -0
  158. /package/{auth → dist/auth}/interceptors/createProcessAxiosRequestErrorInterceptor.js.map +0 -0
  159. /package/{auth → dist/auth}/interceptors/createRetryInterceptor.js +0 -0
  160. /package/{auth → dist/auth}/interceptors/createRetryInterceptor.js.map +0 -0
  161. /package/{auth → dist/auth}/interface.js +0 -0
  162. /package/{auth → dist/auth}/interface.js.map +0 -0
  163. /package/{auth → dist/auth}/utils.js +0 -0
  164. /package/{auth → dist/auth}/utils.js.map +0 -0
  165. /package/{config.js → dist/config.js} +0 -0
  166. /package/{config.js.map → dist/config.js.map} +0 -0
  167. /package/{constants.js → dist/constants.js} +0 -0
  168. /package/{constants.js.map → dist/constants.js.map} +0 -0
  169. /package/{i18n → dist/i18n}/countries.js +0 -0
  170. /package/{i18n → dist/i18n}/countries.js.map +0 -0
  171. /package/{i18n → dist/i18n}/index.js +0 -0
  172. /package/{i18n → dist/i18n}/index.js.map +0 -0
  173. /package/{i18n → dist/i18n}/injectIntlWithShim.js +0 -0
  174. /package/{i18n → dist/i18n}/injectIntlWithShim.js.map +0 -0
  175. /package/{i18n → dist/i18n}/languages.js +0 -0
  176. /package/{i18n → dist/i18n}/languages.js.map +0 -0
  177. /package/{i18n → dist/i18n}/lib.js +0 -0
  178. /package/{i18n → dist/i18n}/lib.js.map +0 -0
  179. /package/{i18n → dist/i18n}/scripts/README.md +0 -0
  180. /package/{i18n → dist/i18n}/scripts/intl-imports.js +0 -0
  181. /package/{i18n → dist/i18n}/scripts/intl-imports.js.map +0 -0
  182. /package/{i18n → dist/i18n}/scripts/transifex-utils.js +0 -0
  183. /package/{i18n → dist/i18n}/scripts/transifex-utils.js.map +0 -0
  184. /package/{index.js → dist/index.js} +0 -0
  185. /package/{index.js.map → dist/index.js.map} +0 -0
  186. /package/{initialize.js → dist/initialize.js} +0 -0
  187. /package/{initialize.js.map → dist/initialize.js.map} +0 -0
  188. /package/{logging → dist/logging}/MockLoggingService.js +0 -0
  189. /package/{logging → dist/logging}/MockLoggingService.js.map +0 -0
  190. /package/{logging → dist/logging}/NewRelicLoggingService.js +0 -0
  191. /package/{logging → dist/logging}/NewRelicLoggingService.js.map +0 -0
  192. /package/{logging → dist/logging}/index.js +0 -0
  193. /package/{logging → dist/logging}/index.js.map +0 -0
  194. /package/{logging → dist/logging}/interface.js +0 -0
  195. /package/{logging → dist/logging}/interface.js.map +0 -0
  196. /package/{pubSub.js → dist/pubSub.js} +0 -0
  197. /package/{pubSub.js.map → dist/pubSub.js.map} +0 -0
  198. /package/{react → dist/react}/AppContext.js +0 -0
  199. /package/{react → dist/react}/AppContext.js.map +0 -0
  200. /package/{react → dist/react}/AppProvider.js +0 -0
  201. /package/{react → dist/react}/AppProvider.js.map +0 -0
  202. /package/{react → dist/react}/AuthenticatedPageRoute.js +0 -0
  203. /package/{react → dist/react}/AuthenticatedPageRoute.js.map +0 -0
  204. /package/{react → dist/react}/ErrorBoundary.js +0 -0
  205. /package/{react → dist/react}/ErrorBoundary.js.map +0 -0
  206. /package/{react → dist/react}/ErrorPage.js +0 -0
  207. /package/{react → dist/react}/ErrorPage.js.map +0 -0
  208. /package/{react → dist/react}/LoginRedirect.js +0 -0
  209. /package/{react → dist/react}/LoginRedirect.js.map +0 -0
  210. /package/{react → dist/react}/OptionalReduxProvider.js +0 -0
  211. /package/{react → dist/react}/OptionalReduxProvider.js.map +0 -0
  212. /package/{react → dist/react}/PageRoute.js +0 -0
  213. /package/{react → dist/react}/PageRoute.js.map +0 -0
  214. /package/{react → dist/react}/hooks.js +0 -0
  215. /package/{react → dist/react}/hooks.js.map +0 -0
  216. /package/{react → dist/react}/index.js +0 -0
  217. /package/{react → dist/react}/index.js.map +0 -0
  218. /package/{scripts → dist/scripts}/GoogleAnalyticsLoader.js +0 -0
  219. /package/{scripts → dist/scripts}/GoogleAnalyticsLoader.js.map +0 -0
  220. /package/{scripts → dist/scripts}/index.js +0 -0
  221. /package/{scripts → dist/scripts}/index.js.map +0 -0
  222. /package/{testing → dist/testing}/index.js +0 -0
  223. /package/{testing → dist/testing}/index.js.map +0 -0
  224. /package/{testing → dist/testing}/initializeMockApp.js +0 -0
  225. /package/{testing → dist/testing}/initializeMockApp.js.map +0 -0
  226. /package/{testing → dist/testing}/mockMessages.js +0 -0
  227. /package/{testing → dist/testing}/mockMessages.js.map +0 -0
  228. /package/{utils.js → dist/utils.js} +0 -0
  229. /package/{utils.js.map → dist/utils.js.map} +0 -0
package/src/pubSub.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * #### Import members from **@edx/frontend-platform**
3
+ *
4
+ * The PubSub module is a thin wrapper around the base functionality of
5
+ * [PubSubJS](https://github.com/mroderick/PubSubJS). For the sake of simplicity and not relying
6
+ * too heavily on implementation-specific features, it maintains a fairly simple API (subscribe,
7
+ * unsubscribe, and publish).
8
+ *
9
+ * Publish/Subscribe events should be used mindfully, especially in relation to application UI
10
+ * frameworks like React. Given React's unidirectional data flow and prop/state management
11
+ * capabilities, using a pub/sub mechanism is at odds with that framework's best practices.
12
+ *
13
+ * That said, we use pub/sub in our application initialization sequence to allow applications to
14
+ * hook into the initialization lifecycle, and we also use them to publish when the application
15
+ * state has changed, i.e., when the config document or user's authentication state have changed.
16
+ *
17
+ * @module PubSub
18
+ */
19
+
20
+ import PubSub from 'pubsub-js';
21
+
22
+ /**
23
+ *
24
+ * @param {string} type
25
+ * @param {function} callback
26
+ * @returns {string} A subscription token that can be passed to `unsubscribe`
27
+ */
28
+ export function subscribe(type, callback) {
29
+ return PubSub.subscribe(type, callback);
30
+ }
31
+
32
+ /**
33
+ *
34
+ * @param {string} token A subscription token provided by `subscribe`
35
+ */
36
+ export function unsubscribe(token) {
37
+ return PubSub.unsubscribe(token);
38
+ }
39
+
40
+ /**
41
+ *
42
+ * @param {string} type
43
+ * @param {Object} data
44
+ */
45
+ export function publish(type, data) {
46
+ return PubSub.publish(type, data);
47
+ }
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+
3
+ /**
4
+ * `AppContext` provides data from `App` in a way that React components can readily consume, even
5
+ * if it's mutable data. `AppContext` contains the following data structure:
6
+ *
7
+ * ```
8
+ * {
9
+ * authenticatedUser: <THE App.authenticatedUser OBJECT>,
10
+ * config: <THE App.config OBJECT>
11
+ * }
12
+ * ```
13
+ * If the `App.authenticatedUser` or `App.config` data changes, `AppContext` will be updated
14
+ * accordingly and pass those changes onto React components using the context.
15
+ *
16
+ * `AppContext` is used in a React application like any other `[React Context](https://reactjs.org/docs/context.html)
17
+ * @memberof module:React
18
+ */
19
+ const AppContext = React.createContext({
20
+ authenticatedUser: null,
21
+ config: {},
22
+ });
23
+
24
+ export default AppContext;
@@ -0,0 +1,93 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Router } from 'react-router-dom';
4
+
5
+ import OptionalReduxProvider from './OptionalReduxProvider';
6
+
7
+ import ErrorBoundary from './ErrorBoundary';
8
+ import AppContext from './AppContext';
9
+ import { useAppEvent, useTrackColorSchemeChoice } from './hooks';
10
+ import { getAuthenticatedUser, AUTHENTICATED_USER_CHANGED } from '../auth';
11
+ import { getConfig } from '../config';
12
+ import { CONFIG_CHANGED } from '../constants';
13
+ import { history } from '../initialize';
14
+ import {
15
+ getLocale,
16
+ getMessages,
17
+ IntlProvider,
18
+ LOCALE_CHANGED,
19
+ } from '../i18n';
20
+
21
+ /**
22
+ * A wrapper component for React-based micro-frontends to initialize a number of common data/
23
+ * context providers.
24
+ *
25
+ * ```
26
+ * subscribe(APP_READY, () => {
27
+ * ReactDOM.render(
28
+ * <AppProvider>
29
+ * <HelloWorld />
30
+ * </AppProvider>
31
+ * )
32
+ * });
33
+ * ```
34
+ *
35
+ * This will provide the following to HelloWorld:
36
+ * - An error boundary as described above.
37
+ * - An `AppContext` provider for React context data.
38
+ * - IntlProvider for @edx/frontend-i18n internationalization
39
+ * - Optionally a redux `Provider`. Will only be included if a `store` property is passed to
40
+ * `AppProvider`.
41
+ * - A `Router` for react-router.
42
+ *
43
+ * @param {Object} props
44
+ * @param {Object} [props.store] A redux store.
45
+ * @memberof module:React
46
+ */
47
+ export default function AppProvider({ store, children }) {
48
+ const [config, setConfig] = useState(getConfig());
49
+ const [authenticatedUser, setAuthenticatedUser] = useState(getAuthenticatedUser());
50
+ const [locale, setLocale] = useState(getLocale());
51
+
52
+ useTrackColorSchemeChoice();
53
+
54
+ useAppEvent(AUTHENTICATED_USER_CHANGED, () => {
55
+ setAuthenticatedUser(getAuthenticatedUser());
56
+ });
57
+
58
+ useAppEvent(CONFIG_CHANGED, () => {
59
+ setConfig(getConfig());
60
+ });
61
+
62
+ useAppEvent(LOCALE_CHANGED, () => {
63
+ setLocale(getLocale());
64
+ });
65
+
66
+ const appContextValue = useMemo(() => ({ authenticatedUser, config, locale }), [authenticatedUser, config, locale]);
67
+
68
+ return (
69
+ <IntlProvider locale={locale} messages={getMessages()}>
70
+ <ErrorBoundary>
71
+ <AppContext.Provider
72
+ value={appContextValue}
73
+ >
74
+ <OptionalReduxProvider store={store}>
75
+ <Router history={history}>
76
+ {children}
77
+ </Router>
78
+ </OptionalReduxProvider>
79
+ </AppContext.Provider>
80
+ </ErrorBoundary>
81
+ </IntlProvider>
82
+ );
83
+ }
84
+
85
+ AppProvider.propTypes = {
86
+ // eslint-disable-next-line react/forbid-prop-types
87
+ store: PropTypes.object,
88
+ children: PropTypes.node.isRequired,
89
+ };
90
+
91
+ AppProvider.defaultProps = {
92
+ store: null,
93
+ };
@@ -0,0 +1,60 @@
1
+ import React, { useContext } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useRouteMatch } from 'react-router-dom';
4
+
5
+ import AppContext from './AppContext';
6
+ import PageRoute from './PageRoute';
7
+ import { getLoginRedirectUrl } from '../auth';
8
+
9
+ /**
10
+ * A react-router route that redirects to the login page when the route becomes active and the user
11
+ * is not authenticated. If the application has been initialized with `requireAuthenticatedUser`
12
+ * false, an authenticatedPageRoute can be used to protect a subset of the application's routes,
13
+ * rather than the entire application.
14
+ *
15
+ * It can optionally accept an override URL to redirect to instead of the login page.
16
+ *
17
+ * Like a `PageRoute`, also calls `sendPageEvent` when the route becomes active.
18
+ *
19
+ * @see PageRoute
20
+ * @see {@link module:frontend-platform/analytics~sendPageEvent}
21
+ * @memberof module:React
22
+ * @param {Object} props
23
+ * @param {string} props.redirectUrl The URL anonymous users should be redirected to, rather than
24
+ * viewing the route's contents.
25
+ */
26
+ export default function AuthenticatedPageRoute({ redirectUrl, ...props }) {
27
+ const { authenticatedUser } = useContext(AppContext);
28
+
29
+ const match = useRouteMatch({
30
+ // eslint-disable-next-line react/prop-types
31
+ path: props.path,
32
+ // eslint-disable-next-line react/prop-types
33
+ exact: props.exact,
34
+ // eslint-disable-next-line react/prop-types
35
+ strict: props.strict,
36
+ // eslint-disable-next-line react/prop-types
37
+ sensitive: props.sensitive,
38
+ });
39
+
40
+ if (authenticatedUser === null) {
41
+ if (match) {
42
+ const destination = redirectUrl || getLoginRedirectUrl(global.location.href);
43
+ global.location.assign(destination);
44
+ }
45
+ // This emulates a Route's way of displaying nothing if the route's path doesn't match the
46
+ // current URL.
47
+ return null;
48
+ }
49
+ return (
50
+ <PageRoute {...props} />
51
+ );
52
+ }
53
+
54
+ AuthenticatedPageRoute.propTypes = {
55
+ redirectUrl: PropTypes.string,
56
+ };
57
+
58
+ AuthenticatedPageRoute.defaultProps = {
59
+ redirectUrl: null,
60
+ };
@@ -0,0 +1,44 @@
1
+ import React, { Component } from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ import { logError } from '../logging';
5
+
6
+ import ErrorPage from './ErrorPage';
7
+
8
+ /**
9
+ * Error boundary component used to log caught errors and display the error page.
10
+ *
11
+ * @memberof module:React
12
+ * @extends {Component}
13
+ */
14
+ export default class ErrorBoundary extends Component {
15
+ constructor(props) {
16
+ super(props);
17
+ this.state = { hasError: false };
18
+ }
19
+
20
+ static getDerivedStateFromError() {
21
+ // Update state so the next render will show the fallback UI.
22
+ return { hasError: true };
23
+ }
24
+
25
+ componentDidCatch(error, info) {
26
+ logError(error, { stack: info.componentStack });
27
+ }
28
+
29
+ render() {
30
+ if (this.state.hasError) {
31
+ return <ErrorPage />;
32
+ }
33
+
34
+ return this.props.children;
35
+ }
36
+ }
37
+
38
+ ErrorBoundary.propTypes = {
39
+ children: PropTypes.node,
40
+ };
41
+
42
+ ErrorBoundary.defaultProps = {
43
+ children: null,
44
+ };
@@ -0,0 +1,76 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import {
4
+ Button, Container, Row, Col,
5
+ } from '@edx/paragon';
6
+
7
+ import { useAppEvent } from './hooks';
8
+ import {
9
+ FormattedMessage,
10
+ IntlProvider,
11
+ getMessages,
12
+ getLocale,
13
+ LOCALE_CHANGED,
14
+ } from '../i18n';
15
+
16
+ /**
17
+ * An error page that displays a generic message for unexpected errors. Also contains a "Try
18
+ * Again" button to refresh the page.
19
+ *
20
+ * @memberof module:React
21
+ * @extends {Component}
22
+ */
23
+ function ErrorPage({
24
+ message,
25
+ }) {
26
+ const [locale, setLocale] = useState(getLocale());
27
+
28
+ useAppEvent(LOCALE_CHANGED, () => {
29
+ setLocale(getLocale());
30
+ });
31
+
32
+ /* istanbul ignore next */
33
+ const reload = () => {
34
+ global.location.reload();
35
+ };
36
+
37
+ return (
38
+ <IntlProvider locale={locale} messages={getMessages()}>
39
+ <Container fluid className="py-5 justify-content-center align-items-start text-center">
40
+ <Row>
41
+ <Col>
42
+ <p className="text-muted">
43
+ <FormattedMessage
44
+ id="unexpected.error.message.text"
45
+ defaultMessage="An unexpected error occurred. Please click the button below to refresh the page."
46
+ description="error message when an unexpected error occurs"
47
+ />
48
+ </p>
49
+ {message && (
50
+ <div role="alert" className="my-4">
51
+ <p>{message}</p>
52
+ </div>
53
+ )}
54
+ <Button onClick={reload}>
55
+ <FormattedMessage
56
+ id="unexpected.error.button.text"
57
+ defaultMessage="Try again"
58
+ description="text for button that tries to reload the app by refreshing the page"
59
+ />
60
+ </Button>
61
+ </Col>
62
+ </Row>
63
+ </Container>
64
+ </IntlProvider>
65
+ );
66
+ }
67
+
68
+ ErrorPage.propTypes = {
69
+ message: PropTypes.string,
70
+ };
71
+
72
+ ErrorPage.defaultProps = {
73
+ message: null,
74
+ };
75
+
76
+ export default ErrorPage;
@@ -0,0 +1,16 @@
1
+ import { useEffect } from 'react';
2
+ import { redirectToLogin } from '../auth';
3
+
4
+ /**
5
+ * A React component that, when rendered, redirects to the login page as a side effect. Uses
6
+ * `redirectToLogin` to perform the redirect.
7
+ *
8
+ * @see {@link module:frontend-platform/auth~redirectToLogin}
9
+ * @memberof module:React
10
+ */
11
+ export default function LoginRedirect() {
12
+ useEffect(() => {
13
+ redirectToLogin(global.location.href);
14
+ }, []);
15
+ return null;
16
+ }
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Provider } from 'react-redux';
4
+
5
+ /**
6
+ * @memberof module:React
7
+ * @param {Object} props
8
+ */
9
+ export default function OptionalReduxProvider({ store, children }) {
10
+ if (store === null) {
11
+ return children;
12
+ }
13
+
14
+ return (
15
+ <Provider store={store}>
16
+ {children}
17
+ </Provider>
18
+ );
19
+ }
20
+
21
+ OptionalReduxProvider.propTypes = {
22
+ store: PropTypes.object, // eslint-disable-line
23
+ children: PropTypes.node.isRequired,
24
+ };
25
+
26
+ OptionalReduxProvider.defaultProps = {
27
+ store: null,
28
+ };
@@ -0,0 +1,31 @@
1
+ /* eslint-disable react/prop-types */
2
+ import React, { useEffect } from 'react';
3
+ import { Route, useRouteMatch } from 'react-router-dom';
4
+ import { sendPageEvent } from '../analytics';
5
+
6
+ /**
7
+ * A react-router Route component that calls `sendPageEvent` when it becomes active.
8
+ *
9
+ * @see {@link module:frontend-platform/analytics~sendPageEvent}
10
+ * @memberof module:React
11
+ * @param {Object} props
12
+ */
13
+ export default function PageRoute(props) {
14
+ const match = useRouteMatch({
15
+ path: props.path,
16
+ exact: props.exact,
17
+ strict: props.strict,
18
+ sensitive: props.sensitive,
19
+ });
20
+
21
+ useEffect(() => {
22
+ if (match) {
23
+ sendPageEvent();
24
+ }
25
+ // eslint-disable-next-line react-hooks/exhaustive-deps
26
+ }, [JSON.stringify(match)]);
27
+
28
+ return (
29
+ <Route {...props} />
30
+ );
31
+ }
@@ -0,0 +1,50 @@
1
+ /* eslint-disable import/prefer-default-export */
2
+ import { useEffect } from 'react';
3
+ import { subscribe, unsubscribe } from '../pubSub';
4
+ import { sendTrackEvent } from '../analytics';
5
+
6
+ /**
7
+ * A React hook that allows functional components to subscribe to application events. This should
8
+ * be used sparingly - for the most part, Context should be used higher-up in the application to
9
+ * provide necessary data to a given component, rather than utilizing a non-React-like Pub/Sub
10
+ * mechanism.
11
+ *
12
+ * @memberof module:React
13
+ * @param {string} type
14
+ * @param {function} callback
15
+ */
16
+ export const useAppEvent = (type, callback) => {
17
+ useEffect(() => {
18
+ const subscriptionToken = subscribe(type, callback);
19
+
20
+ return function cleanup() {
21
+ unsubscribe(subscriptionToken);
22
+ };
23
+ }, [callback, type]);
24
+ };
25
+
26
+ /**
27
+ * A React hook that tracks user's preferred color scheme (light or dark) and sends respective
28
+ * event to the tracking service.
29
+ *
30
+ * @memberof module:React
31
+ */
32
+ export const useTrackColorSchemeChoice = () => {
33
+ useEffect(() => {
34
+ const trackColorSchemeChoice = ({ matches }) => {
35
+ const preferredColorScheme = matches ? 'dark' : 'light';
36
+ sendTrackEvent('openedx.ui.frontend-platform.prefers-color-scheme.selected', { preferredColorScheme });
37
+ };
38
+ const colorSchemeQuery = window.matchMedia?.('(prefers-color-scheme: dark)');
39
+ if (colorSchemeQuery) {
40
+ // send user's initial choice
41
+ trackColorSchemeChoice(colorSchemeQuery);
42
+ colorSchemeQuery.addEventListener('change', trackColorSchemeChoice);
43
+ }
44
+ return () => {
45
+ if (colorSchemeQuery) {
46
+ colorSchemeQuery.removeEventListener('change', trackColorSchemeChoice);
47
+ }
48
+ };
49
+ }, []);
50
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * #### Import members from **@edx/frontend-platform/react**
3
+ * The React module provides a variety of React components, hooks, and contexts for use in an
4
+ * application.
5
+ *
6
+ * @module React
7
+ */
8
+
9
+ export { default as AppContext } from './AppContext';
10
+ export { default as AppProvider } from './AppProvider';
11
+ export { default as AuthenticatedPageRoute } from './AuthenticatedPageRoute';
12
+ export { default as ErrorBoundary } from './ErrorBoundary';
13
+ export { default as ErrorPage } from './ErrorPage';
14
+ export { default as LoginRedirect } from './LoginRedirect';
15
+ export { default as PageRoute } from './PageRoute';
16
+ export { useAppEvent } from './hooks';
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @implements {GoogleAnalyticsLoader}
3
+ * @memberof module:GoogleAnalytics
4
+ */
5
+ class GoogleAnalyticsLoader {
6
+ constructor({ config }) {
7
+ this.analyticsId = config.GOOGLE_ANALYTICS_4_ID;
8
+ }
9
+
10
+ loadScript() {
11
+ if (!this.analyticsId) {
12
+ return;
13
+ }
14
+
15
+ global.googleAnalytics = global.googleAnalytics || [];
16
+ const { googleAnalytics } = global;
17
+
18
+ // If the snippet was invoked do nothing.
19
+ if (googleAnalytics.invoked) {
20
+ return;
21
+ }
22
+
23
+ // Invoked flag, to make sure the snippet
24
+ // is never invoked twice.
25
+ googleAnalytics.invoked = true;
26
+
27
+ googleAnalytics.load = (key, options) => {
28
+ const scriptSrc = document.createElement('script');
29
+ scriptSrc.type = 'text/javascript';
30
+ scriptSrc.async = true;
31
+ scriptSrc.src = `https://www.googletagmanager.com/gtag/js?id=${key}`;
32
+
33
+ const scriptGtag = document.createElement('script');
34
+ scriptGtag.innerHTML = `
35
+ window.dataLayer = window.dataLayer || [];
36
+ function gtag(){dataLayer.push(arguments);}
37
+ gtag('js', new Date());
38
+ gtag('config', '${key}');
39
+ `;
40
+
41
+ // Insert our scripts next to the first script element.
42
+ const first = document.getElementsByTagName('script')[0];
43
+ first.parentNode.insertBefore(scriptSrc, first);
44
+ first.parentNode.insertBefore(scriptGtag, first);
45
+ googleAnalytics._loadOptions = options; // eslint-disable-line no-underscore-dangle
46
+ };
47
+
48
+ // Load GoogleAnalytics with your key.
49
+ googleAnalytics.load(this.analyticsId);
50
+ }
51
+ }
52
+
53
+ export default GoogleAnalyticsLoader;
@@ -0,0 +1,2 @@
1
+ /* eslint-disable import/prefer-default-export */
2
+ export { default as GoogleAnalyticsLoader } from './GoogleAnalyticsLoader';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * #### Import members from **@edx/frontend-platform/testing**
3
+ * The testing module provides helpers for writing tests in Jest.
4
+ *
5
+ * @module Testing
6
+ */
7
+
8
+ export { default as initializeMockApp } from './initializeMockApp';
9
+ export { default as mockMessages } from './mockMessages';
@@ -0,0 +1,77 @@
1
+ import { configure as configureAnalytics, MockAnalyticsService } from '../analytics';
2
+ import { configure as configureI18n } from '../i18n';
3
+ import { configure as configureLogging, MockLoggingService } from '../logging';
4
+ import { configure as configureAuth, MockAuthService } from '../auth';
5
+ import { getConfig } from '../config';
6
+ import mockMessages from './mockMessages';
7
+
8
+ /**
9
+ * Initializes a mock application for component testing. The mock application includes
10
+ * mock analytics, auth, and logging services, and the real i18n service.
11
+ *
12
+ * See MockAnalyticsService, MockAuthService, and MockLoggingService for mock implementation
13
+ * details. For the most part, the analytics and logging services just implement their functions
14
+ * with jest.fn() and do nothing else, whereas the MockAuthService actually has some behavior
15
+ * implemented, it just doesn't make any HTTP calls.
16
+ *
17
+ * Note that this mock application is not sufficient for testing the full application lifecycle or
18
+ * initialization callbacks/custom handlers as described in the 'initialize' function's
19
+ * documentation. It exists merely to set up the mock services that components themselves tend to
20
+ * interact with most often. It could be extended to allow for setting up custom handlers fairly
21
+ * easily, as this functionality would be more-or-less identical to what the real initialize
22
+ * function does.
23
+ *
24
+ * Example:
25
+ *
26
+ * ```
27
+ * import { initializeMockApp } from '@edx/frontend-platform/testing';
28
+ * import { logInfo } from '@edx/frontend-platform/logging';
29
+ *
30
+ * describe('initializeMockApp', () => {
31
+ * it('mocks things correctly', () => {
32
+ * const { loggingService } = initializeMockApp();
33
+ * logInfo('test', {});
34
+ * expect(loggingService.logInfo).toHaveBeenCalledWith('test', {});
35
+ * });
36
+ * });
37
+ * ```
38
+ *
39
+ * @param {Object} [options]
40
+ * @param {*} [options.messages] A i18n-compatible messages object, or an array of such objects. If
41
+ * an array is provided, duplicate keys are resolved with the last-one-in winning.
42
+ * @param {UserData|null} [options.authenticatedUser] A UserData object representing the
43
+ * authenticated user. This is passed directly to MockAuthService.
44
+ * @memberof module:Testing
45
+ */
46
+ export default function initializeMockApp({
47
+ messages = mockMessages,
48
+ authenticatedUser = null,
49
+ } = {}) {
50
+ const loggingService = configureLogging(MockLoggingService, {
51
+ config: getConfig(),
52
+ });
53
+
54
+ const authService = configureAuth(MockAuthService, {
55
+ config: { ...getConfig(), authenticatedUser },
56
+ loggingService,
57
+ });
58
+
59
+ const analyticsService = configureAnalytics(MockAnalyticsService, {
60
+ config: getConfig(),
61
+ httpClient: authService.getAuthenticatedHttpClient(),
62
+ loggingService,
63
+ });
64
+
65
+ // The i18n service configure function has no return value, since there isn't a service class.
66
+ configureI18n({
67
+ config: getConfig(),
68
+ loggingService,
69
+ messages,
70
+ });
71
+
72
+ return {
73
+ analyticsService,
74
+ authService,
75
+ loggingService,
76
+ };
77
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * An empty messages object suitable for fulfilling the i18n service's contract.
3
+ * @memberof module:Testing
4
+ */
5
+ const messages = {
6
+ ar: {},
7
+ 'es-419': {},
8
+ fr: {},
9
+ 'zh-cn': {},
10
+ ca: {},
11
+ he: {},
12
+ id: {},
13
+ 'ko-kr': {},
14
+ pl: {},
15
+ 'pt-br': {},
16
+ ru: {},
17
+ th: {},
18
+ uk: {},
19
+ };
20
+
21
+ export default messages;