@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,244 @@
1
+ import { useEffect, useMemo } from 'react';
2
+
3
+ import {
4
+ useAppConfig,
5
+ getSiteConfig, getLocale, useIntl
6
+ } from '@openedx/frontend-base';
7
+ import PropTypes from 'prop-types';
8
+
9
+ import { getCountryList } from '../../data/countries';
10
+ import { FormFieldRenderer } from '../../field-renderer';
11
+ import { FIELDS } from '../data/constants';
12
+ import messages from '../messages';
13
+ import { CountryField, HonorCode, TermsOfService } from '../RegistrationFields';
14
+
15
+ /**
16
+ * Fields on registration page that are not the default required fields (name, email, username, password).
17
+ * These configurable required fields are defined on the backend using REGISTRATION_EXTRA_FIELDS setting.
18
+ *
19
+ * Country and Honor Code/Terms of Services (if enabled) will appear at the bottom of the form, even if they
20
+ * appear higher in order returned by backend. This is to make the user experience better.
21
+ *
22
+ * */
23
+ const ConfigurableRegistrationForm = (props) => {
24
+ const { formatMessage } = useIntl();
25
+ const {
26
+ email,
27
+ fieldDescriptions,
28
+ fieldErrors,
29
+ formFields,
30
+ setFieldErrors,
31
+ setFormFields,
32
+ autoSubmitRegistrationForm,
33
+ } = props;
34
+ const {
35
+ ENABLE_DYNAMIC_REGISTRATION_FIELDS,
36
+ MARKETING_EMAILS_OPT_IN,
37
+ } = useAppConfig();
38
+
39
+ /**
40
+ * The reason for adding the entry 'United States' is that Chrome auto-fills the form with 'United
41
+ * States' instead of 'United States of America', which does not exist in the country dropdown list.
42
+ * So we added the United States entry in the dropdown list.
43
+ */
44
+ const countryList = useMemo(() => getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }]), []);
45
+
46
+ let showTermsOfServiceAndHonorCode = false;
47
+ let showCountryField = false;
48
+
49
+ const formFieldDescriptions = [];
50
+ const honorCode = [];
51
+ const flags = {
52
+ showConfigurableRegistrationFields: ENABLE_DYNAMIC_REGISTRATION_FIELDS,
53
+ showMarketingEmailOptInCheckbox: MARKETING_EMAILS_OPT_IN,
54
+ };
55
+
56
+ /**
57
+ * If auto submitting register form, we will check tos and honor code fields if they exist for feature parity.
58
+ */
59
+ useEffect(() => {
60
+ if (autoSubmitRegistrationForm) {
61
+ if (Object.keys(fieldDescriptions).includes(FIELDS.HONOR_CODE)) {
62
+ setFormFields(prevState => ({
63
+ ...prevState,
64
+ [FIELDS.HONOR_CODE]: true,
65
+ }));
66
+ }
67
+ if (Object.keys(fieldDescriptions).includes(FIELDS.TERMS_OF_SERVICE)) {
68
+ setFormFields(prevState => ({
69
+ ...prevState,
70
+ [FIELDS.TERMS_OF_SERVICE]: true,
71
+ }));
72
+ }
73
+ }
74
+ }, [autoSubmitRegistrationForm]); // eslint-disable-line react-hooks/exhaustive-deps
75
+
76
+ const handleErrorChange = (fieldName, error) => {
77
+ if (fieldName) {
78
+ setFieldErrors(prevErrors => ({
79
+ ...prevErrors,
80
+ [fieldName]: error,
81
+ }));
82
+ }
83
+ };
84
+
85
+ const handleOnChange = (event, countryValue = null) => {
86
+ const { name } = event.target;
87
+ let value;
88
+ if (countryValue) {
89
+ value = { ...countryValue };
90
+ } else {
91
+ value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
92
+ if (event.target.type === 'checkbox') {
93
+ setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
94
+ }
95
+ }
96
+ setFormFields(prevState => ({ ...prevState, [name]: value }));
97
+ };
98
+
99
+ const handleOnBlur = (event) => {
100
+ const { name, value } = event.target;
101
+ let error = '';
102
+ if ((!value || !value.trim()) && fieldDescriptions[name]?.error_message) {
103
+ error = fieldDescriptions[name].error_message;
104
+ } else if (name === 'confirm_email' && value !== email) {
105
+ error = formatMessage(messages['email.do.not.match']);
106
+ }
107
+ setFieldErrors(prevErrors => ({ ...prevErrors, [name]: error }));
108
+ };
109
+
110
+ const handleOnFocus = (event) => {
111
+ const { name } = event.target;
112
+ setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
113
+ };
114
+
115
+ if (flags.showConfigurableRegistrationFields) {
116
+ Object.keys(fieldDescriptions).forEach(fieldName => {
117
+ const fieldData = fieldDescriptions[fieldName];
118
+ switch (fieldData.name) {
119
+ case FIELDS.COUNTRY:
120
+ showCountryField = true;
121
+ break;
122
+ case FIELDS.HONOR_CODE:
123
+ if (fieldData.type === 'tos_and_honor_code') {
124
+ showTermsOfServiceAndHonorCode = true;
125
+ } else {
126
+ honorCode.push(
127
+ <span key={fieldData.name}>
128
+ <HonorCode
129
+ fieldType={fieldData.type}
130
+ value={formFields[fieldData.name]}
131
+ onChangeHandler={handleOnChange}
132
+ errorMessage={fieldErrors[fieldData.name]}
133
+ />
134
+ </span>,
135
+ );
136
+ }
137
+ break;
138
+ case FIELDS.TERMS_OF_SERVICE:
139
+ honorCode.push(
140
+ <span key={fieldData.name}>
141
+ <TermsOfService
142
+ value={formFields[fieldData.name]}
143
+ onChangeHandler={handleOnChange}
144
+ errorMessage={fieldErrors[fieldData.name]}
145
+ />
146
+ </span>,
147
+ );
148
+ break;
149
+ default:
150
+ formFieldDescriptions.push(
151
+ <span key={fieldData.name}>
152
+ <FormFieldRenderer
153
+ fieldData={fieldData}
154
+ value={formFields[fieldData.name]}
155
+ onChangeHandler={handleOnChange}
156
+ handleBlur={handleOnBlur}
157
+ handleFocus={handleOnFocus}
158
+ errorMessage={fieldErrors[fieldData.name]}
159
+ isRequired
160
+ />
161
+ </span>,
162
+ );
163
+ }
164
+ });
165
+ }
166
+
167
+ if (showCountryField) {
168
+ formFieldDescriptions.push(
169
+ <span key="country">
170
+ <CountryField
171
+ countryList={countryList}
172
+ selectedCountry={formFields.country}
173
+ errorMessage={fieldErrors.country || ''}
174
+ onChangeHandler={handleOnChange}
175
+ handleErrorChange={handleErrorChange}
176
+ onBlurHandler={handleOnBlur}
177
+ onFocusHandler={handleOnFocus}
178
+ />
179
+ </span>,
180
+ );
181
+ }
182
+
183
+ if (flags.showMarketingEmailOptInCheckbox) {
184
+ formFieldDescriptions.push(
185
+ <span key="marketing_email_opt_in">
186
+ <FormFieldRenderer
187
+ fieldData={{
188
+ type: 'checkbox',
189
+ label: formatMessage(messages['registration.opt.in.label'], { siteName: getSiteConfig().siteName }),
190
+ name: 'marketingEmailsOptIn',
191
+ }}
192
+ value={formFields.marketingEmailsOptIn}
193
+ className="form-field--checkbox"
194
+ onChangeHandler={handleOnChange}
195
+ handleBlur={handleOnBlur}
196
+ handleFocus={handleOnFocus}
197
+ />
198
+ </span>,
199
+ );
200
+ }
201
+
202
+ if (showTermsOfServiceAndHonorCode) {
203
+ formFieldDescriptions.push(
204
+ <span key="honor_code">
205
+ <HonorCode fieldType="tos_and_honor_code" onChangeHandler={handleOnChange} value={formFields.honor_code} />
206
+ </span>,
207
+ );
208
+ }
209
+
210
+ return (
211
+ <>
212
+ {formFieldDescriptions}
213
+ <div>
214
+ {honorCode}
215
+ </div>
216
+ </>
217
+ );
218
+ };
219
+
220
+ ConfigurableRegistrationForm.propTypes = {
221
+ email: PropTypes.string.isRequired,
222
+ fieldDescriptions: PropTypes.shape({}),
223
+ fieldErrors: PropTypes.shape({
224
+ country: PropTypes.string,
225
+ }).isRequired,
226
+ formFields: PropTypes.shape({
227
+ country: PropTypes.shape({
228
+ displayValue: PropTypes.string,
229
+ countryCode: PropTypes.string,
230
+ }),
231
+ honor_code: PropTypes.bool,
232
+ marketingEmailsOptIn: PropTypes.bool,
233
+ }).isRequired,
234
+ setFieldErrors: PropTypes.func.isRequired,
235
+ setFormFields: PropTypes.func.isRequired,
236
+ autoSubmitRegistrationForm: PropTypes.bool,
237
+ };
238
+
239
+ ConfigurableRegistrationForm.defaultProps = {
240
+ fieldDescriptions: {},
241
+ autoSubmitRegistrationForm: false,
242
+ };
243
+
244
+ export default ConfigurableRegistrationForm;
@@ -0,0 +1,80 @@
1
+ import { useEffect } from 'react';
2
+
3
+ import { getSiteConfig, useIntl } from '@openedx/frontend-base';
4
+ import { Alert } from '@openedx/paragon';
5
+ import { Error } from '@openedx/paragon/icons';
6
+ import PropTypes from 'prop-types';
7
+
8
+ import { windowScrollTo } from '../../data/utils';
9
+ import {
10
+ FORBIDDEN_REQUEST,
11
+ INTERNAL_SERVER_ERROR,
12
+ TPA_AUTHENTICATION_FAILURE,
13
+ TPA_SESSION_EXPIRED,
14
+ } from '../data/constants';
15
+ import messages from '../messages';
16
+
17
+ const RegistrationFailureMessage = (props) => {
18
+ const { formatMessage } = useIntl();
19
+ const {
20
+ context, errorCode, failureCount,
21
+ } = props;
22
+
23
+ useEffect(() => {
24
+ windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
25
+ }, [errorCode, failureCount]);
26
+
27
+ if (!errorCode) {
28
+ return null;
29
+ }
30
+
31
+ let errorMessage;
32
+ switch (errorCode) {
33
+ case INTERNAL_SERVER_ERROR:
34
+ errorMessage = formatMessage(messages['registration.request.server.error']);
35
+ break;
36
+ case FORBIDDEN_REQUEST:
37
+ errorMessage = formatMessage(messages['registration.rate.limit.error']);
38
+ break;
39
+ case TPA_AUTHENTICATION_FAILURE:
40
+ errorMessage = formatMessage(
41
+ messages['registration.tpa.authentication.failure'],
42
+ {
43
+ platform_name: getSiteConfig().siteName,
44
+ lineBreak: <br />,
45
+ errorMessage: context.errorMessage,
46
+ }
47
+ );
48
+ break;
49
+ case TPA_SESSION_EXPIRED:
50
+ errorMessage = formatMessage(messages['registration.tpa.session.expired'], { provider: context.provider });
51
+ break;
52
+ default:
53
+ errorMessage = formatMessage(messages['registration.empty.form.submission.error']);
54
+ break;
55
+ }
56
+
57
+ return (
58
+ <Alert id="validation-errors" className="mb-5" variant="danger" icon={Error}>
59
+ <Alert.Heading>{formatMessage(messages['registration.request.failure.header'])}</Alert.Heading>
60
+ <p>{errorMessage}</p>
61
+ </Alert>
62
+ );
63
+ };
64
+
65
+ RegistrationFailureMessage.defaultProps = {
66
+ context: {
67
+ errorMessage: null,
68
+ },
69
+ };
70
+
71
+ RegistrationFailureMessage.propTypes = {
72
+ context: PropTypes.shape({
73
+ provider: PropTypes.string,
74
+ errorMessage: PropTypes.string,
75
+ }),
76
+ errorCode: PropTypes.string.isRequired,
77
+ failureCount: PropTypes.number.isRequired,
78
+ };
79
+
80
+ export default RegistrationFailureMessage;