@openedx/frontend-app-authn 1.0.0-alpha.0

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 (177) hide show
  1. package/LICENSE +661 -0
  2. package/README.rst +204 -0
  3. package/package.json +79 -0
  4. package/src/Main.tsx +23 -0
  5. package/src/__mocks__/file.js +1 -0
  6. package/src/__mocks__/svg.js +1 -0
  7. package/src/app.scss +6 -0
  8. package/src/app.ts +43 -0
  9. package/src/base-container/components/default-layout/DefaultLayout.test.jsx +48 -0
  10. package/src/base-container/components/default-layout/LargeLayout.jsx +42 -0
  11. package/src/base-container/components/default-layout/MediumLayout.jsx +49 -0
  12. package/src/base-container/components/default-layout/SmallLayout.jsx +37 -0
  13. package/src/base-container/components/default-layout/index.jsx +3 -0
  14. package/src/base-container/components/default-layout/messages.js +16 -0
  15. package/src/base-container/components/image-layout/ExtraSmallLayout.jsx +31 -0
  16. package/src/base-container/components/image-layout/LargeLayout.jsx +32 -0
  17. package/src/base-container/components/image-layout/MediumLayout.jsx +32 -0
  18. package/src/base-container/components/image-layout/SmallLayout.jsx +31 -0
  19. package/src/base-container/components/image-layout/index.jsx +4 -0
  20. package/src/base-container/components/image-layout/index.scss +37 -0
  21. package/src/base-container/components/image-layout/messages.js +16 -0
  22. package/src/base-container/components/welcome-page-layout/LargeLayout.jsx +46 -0
  23. package/src/base-container/components/welcome-page-layout/MediumLayout.jsx +49 -0
  24. package/src/base-container/components/welcome-page-layout/SmallLayout.jsx +38 -0
  25. package/src/base-container/components/welcome-page-layout/index.jsx +3 -0
  26. package/src/base-container/components/welcome-page-layout/messages.js +21 -0
  27. package/src/base-container/index.jsx +70 -0
  28. package/src/base-container/tests/BaseContainer.test.jsx +44 -0
  29. package/src/common-components/EmbeddedRegistrationRoute.jsx +26 -0
  30. package/src/common-components/EnterpriseSSO.jsx +110 -0
  31. package/src/common-components/FormGroup.jsx +106 -0
  32. package/src/common-components/InstitutionLogistration.jsx +102 -0
  33. package/src/common-components/NotFoundPage.jsx +15 -0
  34. package/src/common-components/PasswordField.jsx +175 -0
  35. package/src/common-components/RedirectLogistration.jsx +94 -0
  36. package/src/common-components/RegisterFaIcons.jsx +8 -0
  37. package/src/common-components/SocialAuthProviders.jsx +75 -0
  38. package/src/common-components/ThirdPartyAuth.jsx +123 -0
  39. package/src/common-components/ThirdPartyAuthAlert.jsx +49 -0
  40. package/src/common-components/UnAuthOnlyRoute.jsx +41 -0
  41. package/src/common-components/data/actions.js +27 -0
  42. package/src/common-components/data/reducers.js +63 -0
  43. package/src/common-components/data/sagas.js +32 -0
  44. package/src/common-components/data/selectors.js +28 -0
  45. package/src/common-components/data/service.js +23 -0
  46. package/src/common-components/data/tests/reducer.test.js +82 -0
  47. package/src/common-components/data/tests/sagas.test.js +71 -0
  48. package/src/common-components/index.jsx +14 -0
  49. package/src/common-components/messages.jsx +127 -0
  50. package/src/common-components/tests/EmbeddedRegistrationRoute.test.jsx +76 -0
  51. package/src/common-components/tests/FormField.test.jsx +289 -0
  52. package/src/common-components/tests/SocialAuthProviders.test.jsx +75 -0
  53. package/src/common-components/tests/ThirdPartyAuthAlert.test.jsx +39 -0
  54. package/src/common-components/tests/UnAuthOnlyRoute.test.jsx +73 -0
  55. package/src/common-components/tests/__snapshots__/SocialAuthProviders.test.jsx.snap +156 -0
  56. package/src/common-components/tests/__snapshots__/ThirdPartyAuthAlert.test.jsx.snap +53 -0
  57. package/src/constants.ts +1 -0
  58. package/src/data/configureStore.js +33 -0
  59. package/src/data/constants.js +38 -0
  60. package/src/data/countries.ts +59 -0
  61. package/src/data/oneTrust.js +3 -0
  62. package/src/data/reducers.js +36 -0
  63. package/src/data/sagas.js +19 -0
  64. package/src/data/tests/cookies.test.js +42 -0
  65. package/src/data/tests/dataUtils.test.js +19 -0
  66. package/src/data/tests/reduxUtils.test.js +14 -0
  67. package/src/data/utils/cookies.js +12 -0
  68. package/src/data/utils/dataUtils.js +81 -0
  69. package/src/data/utils/index.js +11 -0
  70. package/src/data/utils/reduxUtils.js +34 -0
  71. package/src/data/utils/useMobileResponsive.js +24 -0
  72. package/src/field-renderer/FieldRenderer.jsx +156 -0
  73. package/src/field-renderer/index.jsx +2 -0
  74. package/src/field-renderer/tests/FieldRenderer.test.jsx +201 -0
  75. package/src/forgot-password/ForgotPasswordAlert.jsx +97 -0
  76. package/src/forgot-password/ForgotPasswordPage.jsx +193 -0
  77. package/src/forgot-password/data/actions.js +32 -0
  78. package/src/forgot-password/data/reducers.js +58 -0
  79. package/src/forgot-password/data/sagas.js +35 -0
  80. package/src/forgot-password/data/selectors.js +10 -0
  81. package/src/forgot-password/data/service.js +21 -0
  82. package/src/forgot-password/data/tests/reducers.test.js +34 -0
  83. package/src/forgot-password/data/tests/sagas.test.js +67 -0
  84. package/src/forgot-password/index.js +5 -0
  85. package/src/forgot-password/messages.js +128 -0
  86. package/src/forgot-password/tests/ForgotPasswordPage.test.jsx +269 -0
  87. package/src/i18n/index.js +25 -0
  88. package/src/index.ts +3 -0
  89. package/src/login/AccountActivationMessage.jsx +78 -0
  90. package/src/login/ChangePasswordPrompt.jsx +91 -0
  91. package/src/login/LoginFailure.jsx +231 -0
  92. package/src/login/LoginPage.jsx +368 -0
  93. package/src/login/data/actions.js +39 -0
  94. package/src/login/data/constants.js +20 -0
  95. package/src/login/data/reducers.js +76 -0
  96. package/src/login/data/sagas.js +45 -0
  97. package/src/login/data/service.js +24 -0
  98. package/src/login/data/tests/reducers.test.js +155 -0
  99. package/src/login/data/tests/sagas.test.js +110 -0
  100. package/src/login/index.js +5 -0
  101. package/src/login/messages.jsx +211 -0
  102. package/src/login/tests/AccountActivationMessage.test.jsx +122 -0
  103. package/src/login/tests/ChangePasswordPrompt.test.jsx +75 -0
  104. package/src/login/tests/LoginFailure.test.jsx +323 -0
  105. package/src/login/tests/LoginPage.test.jsx +839 -0
  106. package/src/logistration/Logistration.jsx +189 -0
  107. package/src/logistration/Logistration.test.jsx +275 -0
  108. package/src/progressive-profiling/ProgressiveProfiling.jsx +313 -0
  109. package/src/progressive-profiling/ProgressiveProfilingPageModal.jsx +56 -0
  110. package/src/progressive-profiling/data/actions.js +22 -0
  111. package/src/progressive-profiling/data/reducers.js +38 -0
  112. package/src/progressive-profiling/data/sagas.js +24 -0
  113. package/src/progressive-profiling/data/selectors.js +14 -0
  114. package/src/progressive-profiling/data/service.js +17 -0
  115. package/src/progressive-profiling/index.js +5 -0
  116. package/src/progressive-profiling/messages.jsx +57 -0
  117. package/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx +322 -0
  118. package/src/register/RegistrationFields/CountryField/CountryField.jsx +164 -0
  119. package/src/register/RegistrationFields/CountryField/CountryField.test.jsx +204 -0
  120. package/src/register/RegistrationFields/CountryField/validator.js +32 -0
  121. package/src/register/RegistrationFields/EmailField/EmailField.jsx +140 -0
  122. package/src/register/RegistrationFields/EmailField/EmailField.test.jsx +236 -0
  123. package/src/register/RegistrationFields/EmailField/constants.js +131 -0
  124. package/src/register/RegistrationFields/EmailField/validator.js +126 -0
  125. package/src/register/RegistrationFields/EmailField/validator.test.js +39 -0
  126. package/src/register/RegistrationFields/HonorCodeField/HonorCode.jsx +104 -0
  127. package/src/register/RegistrationFields/HonorCodeField/HonorCode.test.jsx +70 -0
  128. package/src/register/RegistrationFields/NameField/NameField.jsx +68 -0
  129. package/src/register/RegistrationFields/NameField/NameField.test.jsx +146 -0
  130. package/src/register/RegistrationFields/NameField/validator.js +22 -0
  131. package/src/register/RegistrationFields/TermsOfServiceField/TermsOfService.jsx +58 -0
  132. package/src/register/RegistrationFields/TermsOfServiceField/TermsOfService.test.jsx +55 -0
  133. package/src/register/RegistrationFields/UsernameField/UsernameField.jsx +162 -0
  134. package/src/register/RegistrationFields/UsernameField/UsernameField.test.jsx +319 -0
  135. package/src/register/RegistrationFields/UsernameField/validator.js +16 -0
  136. package/src/register/RegistrationFields/index.js +6 -0
  137. package/src/register/RegistrationPage.jsx +428 -0
  138. package/src/register/RegistrationPage.test.jsx +854 -0
  139. package/src/register/components/ConfigurableRegistrationForm.jsx +244 -0
  140. package/src/register/components/RegistrationFailure.jsx +80 -0
  141. package/src/register/components/tests/ConfigurableRegistrationForm.test.jsx +401 -0
  142. package/src/register/components/tests/RegistrationFailure.test.jsx +212 -0
  143. package/src/register/components/tests/ThirdPartyAuth.test.jsx +439 -0
  144. package/src/register/data/actions.js +85 -0
  145. package/src/register/data/constants.js +13 -0
  146. package/src/register/data/reducers.js +140 -0
  147. package/src/register/data/sagas.js +58 -0
  148. package/src/register/data/selectors.js +34 -0
  149. package/src/register/data/service.js +45 -0
  150. package/src/register/data/tests/reducers.test.js +277 -0
  151. package/src/register/data/tests/sagas.test.js +239 -0
  152. package/src/register/data/tests/utils.test.js +98 -0
  153. package/src/register/data/utils.js +153 -0
  154. package/src/register/index.js +4 -0
  155. package/src/register/messages.jsx +206 -0
  156. package/src/reset-password/ResetPasswordFailure.jsx +55 -0
  157. package/src/reset-password/ResetPasswordPage.jsx +224 -0
  158. package/src/reset-password/ResetPasswordSuccess.jsx +19 -0
  159. package/src/reset-password/data/actions.js +50 -0
  160. package/src/reset-password/data/constants.js +16 -0
  161. package/src/reset-password/data/reducers.js +44 -0
  162. package/src/reset-password/data/sagas.js +67 -0
  163. package/src/reset-password/data/selectors.js +10 -0
  164. package/src/reset-password/data/service.js +61 -0
  165. package/src/reset-password/data/tests/sagas.test.js +185 -0
  166. package/src/reset-password/index.js +5 -0
  167. package/src/reset-password/messages.js +88 -0
  168. package/src/reset-password/tests/ResetPasswordPage.test.jsx +241 -0
  169. package/src/routes.jsx +79 -0
  170. package/src/sass/_base_component.scss +202 -0
  171. package/src/sass/_forgot_password.scss +3 -0
  172. package/src/sass/_login_page.scss +3 -0
  173. package/src/sass/_progressive_profiling_page.scss +30 -0
  174. package/src/sass/_registration.scss +123 -0
  175. package/src/sass/_reset_password.scss +3 -0
  176. package/src/sass/_style.scss +465 -0
  177. package/src/setupTest.js +68 -0
@@ -0,0 +1,313 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { connect } from 'react-redux';
3
+
4
+ import {
5
+ AxiosJwtAuthService,
6
+ configureAuth,
7
+ useAppConfig,
8
+ getAuthenticatedUser,
9
+ getSiteConfig,
10
+ getLoggingService,
11
+ identifyAuthenticatedUser,
12
+ sendPageEvent,
13
+ sendTrackEvent,
14
+ snakeCaseObject,
15
+ useIntl
16
+ } from '@openedx/frontend-base';
17
+ import {
18
+ Alert,
19
+ Form,
20
+ Hyperlink,
21
+ Spinner,
22
+ StatefulButton,
23
+ } from '@openedx/paragon';
24
+ import { Error } from '@openedx/paragon/icons';
25
+ import PropTypes from 'prop-types';
26
+ import { Helmet } from 'react-helmet';
27
+ import { useLocation } from 'react-router-dom';
28
+
29
+ import BaseContainer from '../base-container';
30
+ import { RedirectLogistration } from '../common-components';
31
+ import { getThirdPartyAuthContext } from '../common-components/data/actions';
32
+ import {
33
+ COMPLETE_STATE,
34
+ DEFAULT_REDIRECT_URL,
35
+ DEFAULT_STATE,
36
+ FAILURE_STATE,
37
+ PENDING_STATE,
38
+ } from '../data/constants';
39
+ import isOneTrustFunctionalCookieEnabled from '../data/oneTrust';
40
+ import { getAllPossibleQueryParams, isHostAvailableInQueryParams } from '../data/utils';
41
+ import { FormFieldRenderer } from '../field-renderer';
42
+ import { saveUserProfile } from './data/actions';
43
+ import { welcomePageContextSelector } from './data/selectors';
44
+ import messages from './messages';
45
+ import ProgressiveProfilingPageModal from './ProgressiveProfilingPageModal';
46
+
47
+ const ProgressiveProfiling = (props) => {
48
+ const { formatMessage } = useIntl();
49
+ const {
50
+ getFieldDataFromBackend,
51
+ submitState,
52
+ showError,
53
+ welcomePageContext,
54
+ welcomePageContextApiStatus,
55
+ } = props;
56
+ const {
57
+ SEARCH_CATALOG_URL,
58
+ AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK
59
+ } = useAppConfig();
60
+ const location = useLocation();
61
+ const registrationEmbedded = isHostAvailableInQueryParams();
62
+
63
+ const queryParams = getAllPossibleQueryParams();
64
+ const authenticatedUser = getAuthenticatedUser() || location.state?.authenticatedUser;
65
+ const functionalCookiesConsent = isOneTrustFunctionalCookieEnabled();
66
+
67
+ const [registrationResult, setRegistrationResult] = useState({ redirectUrl: '' });
68
+ const [formFieldData, setFormFieldData] = useState({ fields: {}, extendedProfile: [] });
69
+ const [values, setValues] = useState({});
70
+ const [showModal, setShowModal] = useState(false);
71
+
72
+ useEffect(() => {
73
+ if (registrationEmbedded) {
74
+ getFieldDataFromBackend({ is_welcome_page: true, next: queryParams?.next });
75
+ } else {
76
+ configureAuth(AxiosJwtAuthService, { mockLoggingService: getLoggingService(), config: getSiteConfig() });
77
+ }
78
+ }, [registrationEmbedded, getFieldDataFromBackend, queryParams?.next]);
79
+
80
+ useEffect(() => {
81
+ const registrationResponse = location.state?.registrationResult;
82
+ if (registrationResponse) {
83
+ setRegistrationResult(registrationResponse);
84
+ setFormFieldData({
85
+ fields: location.state?.optionalFields.fields,
86
+ extendedProfile: location.state?.optionalFields.extended_profile,
87
+ });
88
+ }
89
+ }, [location.state]);
90
+
91
+ useEffect(() => {
92
+ if (registrationEmbedded && Object.keys(welcomePageContext).includes('fields')) {
93
+ setFormFieldData({
94
+ fields: welcomePageContext.fields,
95
+ extendedProfile: welcomePageContext.extended_profile,
96
+ });
97
+ const nextUrl = welcomePageContext.nextUrl ? welcomePageContext.nextUrl : SEARCH_CATALOG_URL;
98
+ setRegistrationResult({ redirectUrl: nextUrl });
99
+ }
100
+ }, [registrationEmbedded, welcomePageContext]);
101
+
102
+ useEffect(() => {
103
+ if (authenticatedUser?.userId) {
104
+ identifyAuthenticatedUser(authenticatedUser.userId);
105
+ sendPageEvent('login_and_registration', 'welcome');
106
+ }
107
+ }, [authenticatedUser]);
108
+
109
+ if (
110
+ !authenticatedUser
111
+ || !(location.state?.registrationResult || registrationEmbedded)
112
+ || welcomePageContextApiStatus === FAILURE_STATE
113
+ || (welcomePageContextApiStatus === COMPLETE_STATE && !Object.keys(welcomePageContext).includes('fields'))
114
+ ) {
115
+ const DASHBOARD_URL = getSiteConfig().lmsBaseUrl.concat(DEFAULT_REDIRECT_URL);
116
+ global.location.assign(DASHBOARD_URL);
117
+ return null;
118
+ }
119
+
120
+ const handleSubmit = (e) => {
121
+ e.preventDefault();
122
+ window.history.replaceState(location.state, null, '');
123
+ const payload = { ...values, extendedProfile: [] };
124
+ if (Object.keys(formFieldData.extendedProfile).length > 0) {
125
+ formFieldData.extendedProfile.forEach(fieldName => {
126
+ if (values[fieldName]) {
127
+ payload.extendedProfile.push({ fieldName, fieldValue: values[fieldName] });
128
+ }
129
+ delete payload[fieldName];
130
+ });
131
+ }
132
+ props.saveUserProfile(authenticatedUser.username, snakeCaseObject(payload));
133
+
134
+ sendTrackEvent(
135
+ 'edx.bi.welcome.page.submit.clicked',
136
+ {
137
+ isGenderSelected: !!values.gender,
138
+ isYearOfBirthSelected: !!values.year_of_birth,
139
+ isLevelOfEducationSelected: !!values.level_of_education,
140
+ isWorkExperienceSelected: !!values.work_experience,
141
+ host: queryParams?.host || '',
142
+ },
143
+ );
144
+ };
145
+
146
+ const handleSkip = (e) => {
147
+ e.preventDefault();
148
+ window.history.replaceState(location.state, null, '');
149
+ setShowModal(true);
150
+ sendTrackEvent(
151
+ 'edx.bi.welcome.page.skip.link.clicked',
152
+ {
153
+ host: queryParams?.host || '',
154
+ },
155
+ );
156
+ };
157
+
158
+ const onChangeHandler = (e) => {
159
+ if (e.target.type === 'checkbox') {
160
+ setValues({ ...values, [e.target.name]: e.target.checked });
161
+ } else {
162
+ setValues({ ...values, [e.target.name]: e.target.value });
163
+ }
164
+ };
165
+
166
+ const formFields = Object.keys(formFieldData.fields).map((fieldName) => {
167
+ const fieldData = formFieldData.fields[fieldName];
168
+ return (
169
+ <span key={fieldData.name}>
170
+ <FormFieldRenderer
171
+ fieldData={fieldData}
172
+ value={values[fieldData.name]}
173
+ onChangeHandler={onChangeHandler}
174
+ />
175
+ </span>
176
+ );
177
+ });
178
+
179
+ return (
180
+ <BaseContainer showWelcomeBanner fullName={authenticatedUser?.fullName || authenticatedUser?.name}>
181
+ <Helmet>
182
+ <title>{formatMessage(
183
+ messages['progressive.profiling.page.title'],
184
+ { siteName: getSiteConfig().siteName }
185
+ )}
186
+ </title>
187
+ </Helmet>
188
+ <ProgressiveProfilingPageModal isOpen={showModal} redirectUrl={registrationResult.redirectUrl} />
189
+ {(props.shouldRedirect && welcomePageContext.nextUrl) && (
190
+ <RedirectLogistration
191
+ success
192
+ redirectUrl={registrationResult.redirectUrl}
193
+ />
194
+ )}
195
+ {props.shouldRedirect && (
196
+ <RedirectLogistration
197
+ success
198
+ redirectUrl={registrationResult.redirectUrl}
199
+ educationLevel={values?.level_of_education}
200
+ userId={authenticatedUser?.userId}
201
+ />
202
+ )}
203
+ <div className="mw-xs m-4 pp-page-content">
204
+ {registrationEmbedded && welcomePageContextApiStatus === PENDING_STATE ? (
205
+ <Spinner animation="border" variant="primary" id="tpa-spinner" />
206
+ ) : (
207
+ <>
208
+ <div>
209
+ <h2 className="pp-page__heading text-primary">{formatMessage(messages['progressive.profiling.page.heading'])}</h2>
210
+ </div><hr className="border-light-700 mb-4" />
211
+ {showError ? (
212
+ <Alert id="pp-page-errors" className="mb-3" variant="danger" icon={Error}>
213
+ <Alert.Heading>{formatMessage(messages['welcome.page.error.heading'])}</Alert.Heading>
214
+ <p>{formatMessage(messages['welcome.page.error.message'])}</p>
215
+ </Alert>
216
+ ) : null}
217
+ <Form>
218
+ {formFields}
219
+ {(AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK) && (
220
+ <span className="pp-page__support-link">
221
+ <Hyperlink
222
+ isInline
223
+ variant="muted"
224
+ destination={AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK}
225
+ target="_blank"
226
+ showLaunchIcon={false}
227
+ onClick={() => (sendTrackEvent('edx.bi.welcome.page.support.link.clicked'))}
228
+ >
229
+ {formatMessage(messages['optional.fields.information.link'])}
230
+ </Hyperlink>
231
+ </span>
232
+ )}
233
+ <div className="d-flex mt-4 mb-3">
234
+ <StatefulButton
235
+ type="submit"
236
+ variant="brand"
237
+ className="pp-page__button-width"
238
+ state={submitState}
239
+ labels={{
240
+ default: formatMessage(messages['optional.fields.submit.button']),
241
+ pending: '',
242
+ }}
243
+ onClick={handleSubmit}
244
+ onMouseDown={(e) => e.preventDefault()}
245
+ />
246
+ <StatefulButton
247
+ className="text-gray-700 font-weight-500"
248
+ type="submit"
249
+ variant="link"
250
+ labels={{
251
+ default: formatMessage(messages['optional.fields.skip.button']),
252
+ }}
253
+ onClick={handleSkip}
254
+ onMouseDown={(e) => e.preventDefault()}
255
+ />
256
+ </div>
257
+ </Form>
258
+ </>
259
+ )}
260
+
261
+ </div>
262
+ </BaseContainer>
263
+ );
264
+ };
265
+
266
+ ProgressiveProfiling.propTypes = {
267
+ authenticatedUser: PropTypes.shape({
268
+ username: PropTypes.string,
269
+ userId: PropTypes.number,
270
+ fullName: PropTypes.string,
271
+ }),
272
+ showError: PropTypes.bool,
273
+ shouldRedirect: PropTypes.bool,
274
+ submitState: PropTypes.string,
275
+ welcomePageContext: PropTypes.shape({
276
+ extended_profile: PropTypes.arrayOf(PropTypes.string),
277
+ fields: PropTypes.shape({}),
278
+ nextUrl: PropTypes.string,
279
+ }),
280
+ welcomePageContextApiStatus: PropTypes.string,
281
+ // Actions
282
+ getFieldDataFromBackend: PropTypes.func.isRequired,
283
+ saveUserProfile: PropTypes.func.isRequired,
284
+ };
285
+
286
+ ProgressiveProfiling.defaultProps = {
287
+ authenticatedUser: {},
288
+ shouldRedirect: false,
289
+ showError: false,
290
+ submitState: DEFAULT_STATE,
291
+ welcomePageContext: {},
292
+ welcomePageContextApiStatus: PENDING_STATE,
293
+ };
294
+
295
+ const mapStateToProps = state => {
296
+ const welcomePageStore = state.welcomePage;
297
+
298
+ return {
299
+ shouldRedirect: welcomePageStore.success,
300
+ showError: welcomePageStore.showError,
301
+ submitState: welcomePageStore.submitState,
302
+ welcomePageContext: welcomePageContextSelector(state),
303
+ welcomePageContextApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
304
+ };
305
+ };
306
+
307
+ export default connect(
308
+ mapStateToProps,
309
+ {
310
+ saveUserProfile,
311
+ getFieldDataFromBackend: getThirdPartyAuthContext,
312
+ },
313
+ )(ProgressiveProfiling);
@@ -0,0 +1,56 @@
1
+ import { getSiteConfig, useIntl } from '@openedx/frontend-base';
2
+ import { ActionRow, Button, ModalDialog } from '@openedx/paragon';
3
+ import PropTypes from 'prop-types';
4
+
5
+ import messages from './messages';
6
+
7
+ const ProgressiveProfilingPageModal = (props) => {
8
+ const { formatMessage } = useIntl();
9
+ const { isOpen, redirectUrl } = props;
10
+ const platformName = getSiteConfig().siteName;
11
+
12
+ const handleSubmit = (e) => {
13
+ e.preventDefault();
14
+ window.location.href = redirectUrl;
15
+ };
16
+
17
+ return (
18
+ <ModalDialog
19
+ title={formatMessage(messages['modal.title'])}
20
+ isOpen={isOpen}
21
+ onClose={() => { }}
22
+ size="sm"
23
+ variant="default"
24
+ hasCloseButton={false}
25
+ >
26
+ <ModalDialog.Header>
27
+ <ModalDialog.Title>
28
+ {formatMessage(messages['modal.title'])}
29
+ </ModalDialog.Title>
30
+ </ModalDialog.Header>
31
+
32
+ <ModalDialog.Body>
33
+ {formatMessage(messages['modal.description'])}
34
+ </ModalDialog.Body>
35
+
36
+ <ModalDialog.Footer>
37
+ <ActionRow>
38
+ <Button onClick={handleSubmit} variant="primary">
39
+ {formatMessage(messages['continue.to.platform'], { platformName })}
40
+ </Button>
41
+ </ActionRow>
42
+ </ModalDialog.Footer>
43
+ </ModalDialog>
44
+ );
45
+ };
46
+
47
+ ProgressiveProfilingPageModal.propTypes = {
48
+ isOpen: PropTypes.bool,
49
+ redirectUrl: PropTypes.string.isRequired,
50
+ };
51
+
52
+ ProgressiveProfilingPageModal.defaultProps = {
53
+ isOpen: false,
54
+ };
55
+
56
+ export default ProgressiveProfilingPageModal;
@@ -0,0 +1,22 @@
1
+ import { AsyncActionType } from '../../data/utils';
2
+
3
+ export const GET_FIELDS_DATA = new AsyncActionType('FIELD_DESCRIPTION', 'GET_FIELDS_DATA');
4
+ export const SAVE_USER_PROFILE = new AsyncActionType('USER_PROFILE', 'SAVE_USER_PROFILE');
5
+
6
+ // save additional user information
7
+ export const saveUserProfile = (username, data) => ({
8
+ type: SAVE_USER_PROFILE.BASE,
9
+ payload: { username, data },
10
+ });
11
+
12
+ export const saveUserProfileBegin = () => ({
13
+ type: SAVE_USER_PROFILE.BEGIN,
14
+ });
15
+
16
+ export const saveUserProfileSuccess = () => ({
17
+ type: SAVE_USER_PROFILE.SUCCESS,
18
+ });
19
+
20
+ export const saveUserProfileFailure = () => ({
21
+ type: SAVE_USER_PROFILE.FAILURE,
22
+ });
@@ -0,0 +1,38 @@
1
+ import { SAVE_USER_PROFILE } from './actions';
2
+ import {
3
+ DEFAULT_STATE, PENDING_STATE,
4
+ } from '../../data/constants';
5
+
6
+ export const defaultState = {
7
+ extendedProfile: [],
8
+ fieldDescriptions: {},
9
+ success: false,
10
+ submitState: DEFAULT_STATE,
11
+ showError: false,
12
+ };
13
+
14
+ const reducer = (state = defaultState, action = {}) => {
15
+ switch (action.type) {
16
+ case SAVE_USER_PROFILE.BEGIN:
17
+ return {
18
+ ...state,
19
+ submitState: PENDING_STATE,
20
+ };
21
+ case SAVE_USER_PROFILE.SUCCESS:
22
+ return {
23
+ ...state,
24
+ success: true,
25
+ showError: false,
26
+ };
27
+ case SAVE_USER_PROFILE.FAILURE:
28
+ return {
29
+ ...state,
30
+ submitState: DEFAULT_STATE,
31
+ showError: true,
32
+ };
33
+ default:
34
+ return state;
35
+ }
36
+ };
37
+
38
+ export default reducer;
@@ -0,0 +1,24 @@
1
+ import { call, put, takeEvery } from 'redux-saga/effects';
2
+
3
+ import {
4
+ SAVE_USER_PROFILE,
5
+ saveUserProfileBegin,
6
+ saveUserProfileFailure,
7
+ saveUserProfileSuccess,
8
+ } from './actions';
9
+ import { patchAccount } from './service';
10
+
11
+ export function* saveUserProfileInformation(action) {
12
+ try {
13
+ yield put(saveUserProfileBegin());
14
+ yield call(patchAccount, action.payload.username, action.payload.data);
15
+
16
+ yield put(saveUserProfileSuccess());
17
+ } catch (e) {
18
+ yield put(saveUserProfileFailure());
19
+ }
20
+ }
21
+
22
+ export default function* saga() {
23
+ yield takeEvery(SAVE_USER_PROFILE.BASE, saveUserProfileInformation);
24
+ }
@@ -0,0 +1,14 @@
1
+ import { createSelector } from 'reselect';
2
+
3
+ export const storeName = 'commonComponents';
4
+
5
+ export const commonComponentsSelector = state => ({ ...state[storeName] });
6
+
7
+ export const welcomePageContextSelector = createSelector(
8
+ commonComponentsSelector,
9
+ commonComponents => ({
10
+ fields: commonComponents.optionalFields.fields,
11
+ extended_profile: commonComponents.optionalFields.extended_profile,
12
+ nextUrl: commonComponents.thirdPartyAuthContext.welcomePageRedirectUrl,
13
+ }),
14
+ );
@@ -0,0 +1,17 @@
1
+ import { getAuthenticatedHttpClient, getSiteConfig } from '@openedx/frontend-base';
2
+
3
+ export async function patchAccount(username, commitValues) {
4
+ const requestConfig = {
5
+ headers: { 'Content-Type': 'application/merge-patch+json' },
6
+ };
7
+
8
+ await getAuthenticatedHttpClient()
9
+ .patch(
10
+ `${getSiteConfig().lmsBaseUrl}/api/user/v1/accounts/${username}`,
11
+ commitValues,
12
+ requestConfig,
13
+ )
14
+ .catch((error) => {
15
+ throw (error);
16
+ });
17
+ }
@@ -0,0 +1,5 @@
1
+ export const storeName = 'welcomePage';
2
+
3
+ export { default as ProgressiveProfiling } from './ProgressiveProfiling';
4
+ export { default as reducer } from './data/reducers';
5
+ export { default as saga } from './data/sagas';
@@ -0,0 +1,57 @@
1
+ import { defineMessages } from '@openedx/frontend-base';
2
+
3
+ const messages = defineMessages({
4
+ 'progressive.profiling.page.title': {
5
+ id: 'progressive.profiling.page.title',
6
+ defaultMessage: 'Welcome | {siteName}',
7
+ description: 'progressive profiling page title',
8
+ },
9
+ 'progressive.profiling.page.heading': {
10
+ id: 'progressive.profiling.page.heading',
11
+ defaultMessage: 'A few questions for you will help us get smarter.',
12
+ description: 'The page heading for the progressive profiling page.',
13
+ },
14
+ 'optional.fields.information.link': {
15
+ id: 'optional.fields.information.link',
16
+ defaultMessage: 'Learn more about how we use this information.',
17
+ description: 'Optional fields page information link',
18
+ },
19
+ 'optional.fields.submit.button': {
20
+ id: 'optional.fields.submit.button',
21
+ defaultMessage: 'Submit',
22
+ description: 'Submit button text',
23
+ },
24
+ 'optional.fields.skip.button': {
25
+ id: 'optional.fields.skip.button',
26
+ defaultMessage: 'Skip for now',
27
+ description: 'Skip button text',
28
+ },
29
+ // modal dialog box
30
+ 'continue.to.platform': {
31
+ id: 'continue.to.platform',
32
+ defaultMessage: 'Continue to {platformName}',
33
+ description: 'Button text for modal when user chooses "skip for now" option',
34
+ },
35
+ 'modal.title': {
36
+ id: 'modal.title',
37
+ defaultMessage: 'Thanks for letting us know.',
38
+ description: 'Heading for welcome page modal',
39
+ },
40
+ 'modal.description': {
41
+ id: 'modal.description',
42
+ defaultMessage: 'You can complete your profile in settings at any time if you change your mind.',
43
+ description: 'Modal body text',
44
+ },
45
+ // error message
46
+ 'welcome.page.error.heading': {
47
+ id: 'welcome.page.error.heading',
48
+ defaultMessage: 'We couldn\'t update your profile',
49
+ description: 'Error message heading',
50
+ },
51
+ 'welcome.page.error.message': {
52
+ id: 'welcome.page.error.message',
53
+ defaultMessage: 'An error occurred. You can complete your profile in settings at any time.',
54
+ description: 'Error message body',
55
+ },
56
+ });
57
+ export default messages;