Compare commits

..

9 Commits

Author SHA1 Message Date
Stanislav
48122ff99d fix: Missed favicon in Safari (#1080)
Co-authored-by: Stanislav Lunyachek <lunyachek@MacBook-Pro-M1.local>
2023-12-08 10:07:31 +05:00
vladislavkeblysh
3153cff4ff fix: fixed spacing in tablet version (quince.master) (#1107) 2023-11-28 11:41:06 +05:00
Ihor Romaniuk
919d98df79 fix: content centering and z-index position on adaptation (#1093) 2023-11-27 12:26:56 +05:00
Stanislav
c7adcecb8a fix: Enabling the ability to log in with a username consisting of 2 characters (#1098)
Co-authored-by: Stanislav Lunyachek <lunyachek@MacBook-Pro-M1.local>
2023-11-22 16:38:24 +05:00
Ghassan Maslamani
db4eeac266 fix: form submission when country is not requried (#1099)
When country is not required to submit, the displayValue check
will always return true. This fixes it by only check it if it's
needed through the config `SHOW_CONFIGURABLE_EDX_FIELDS`

This issue might has been discoverd at edx.org becuase edx.org
requires the Country to be filled when creating an account,
however this is not the case for Open edX by default, hence the
issue reported below

Ref: openedx/wg-build-test-release/issues/318
2023-11-17 09:25:29 -03:00
Syed Ali Abbas Zaidi
60e2119116 chore: bump frontend-platform (#1076) 2023-10-18 11:23:16 +05:00
Zainab Amir
7b4714a22a feat: fix recommendations card subtitle (#1070) 2023-10-06 15:58:29 +05:00
Zainab Amir
332d6abee7 feat: updated default value for USER_RETENTION_COOKIE_NAME (#1059)
Moved the value to internal configuration

VAN-1624
2023-09-12 14:10:43 +05:00
Zainab Amir
4df13cf0b7 feat: remove optimizely event (#1051)
VAN-1624
2023-09-12 13:32:29 +05:00
40 changed files with 248 additions and 1042 deletions

2
.env
View File

@@ -15,7 +15,7 @@ SEGMENT_KEY=''
SITE_NAME=null
INFO_EMAIL=''
# ***** Cookies *****
REGISTER_CONVERSION_COOKIE_NAME=null
USER_RETENTION_COOKIE_NAME=null
# ***** Links *****
LOGIN_ISSUE_SUPPORT_LINK=''
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null

View File

@@ -20,7 +20,6 @@ SEGMENT_KEY=''
SITE_NAME='Your Platform Name Here'
INFO_EMAIL='info@example.com'
# ***** Cookies *****
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
SESSION_COOKIE_DOMAIN='localhost'
USER_INFO_COOKIE_NAME='edx-user-info'
# ***** Links *****

View File

@@ -16,6 +16,5 @@ ORDER_HISTORY_URL='http://localhost:1996/orders'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=''
SITE_NAME='Your Platform Name Here'
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
APP_ID=''
MFE_CONFIG_API_URL=''

10
package-lock.json generated
View File

@@ -10,7 +10,7 @@
"license": "AGPL-3.0",
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
"@edx/frontend-platform": "^5.0.0",
"@edx/frontend-platform": "^5.5.4",
"@edx/paragon": "20.46.2",
"@fortawesome/fontawesome-svg-core": "6.4.2",
"@fortawesome/free-brands-svg-icons": "6.4.2",
@@ -3350,9 +3350,9 @@
}
},
"node_modules/@edx/frontend-platform": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-5.0.0.tgz",
"integrity": "sha512-DD9/B4rnC3BKPiWlbEFF1JIYFbWC6vUBKTyN8sf4khi4DNhhWhsobk+iNeCWNzF9UgCPRbniIqesdV1F9NXNZw==",
"version": "5.5.4",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-5.5.4.tgz",
"integrity": "sha512-Yum+oST7XfDwDnDhBnzeR/mjp6O+G0g+5AZtIJ1BdTKQH1z9FObfim/pfoiI9STiYlguVpeWMkzWuca/QMLO/Q==",
"dependencies": {
"@cospired/i18n-iso-languages": "4.1.0",
"@formatjs/intl-pluralrules": "4.3.3",
@@ -3380,7 +3380,7 @@
},
"peerDependencies": {
"@edx/frontend-build": ">= 8.1.0 || ^12.9.0-alpha.1",
"@edx/paragon": ">= 10.0.0 < 21.0.0",
"@edx/paragon": ">= 10.0.0 < 22.0.0",
"prop-types": "^15.7.2",
"react": "^16.9.0 || ^17.0.0",
"react-dom": "^16.9.0 || ^17.0.0",

View File

@@ -33,7 +33,7 @@
},
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
"@edx/frontend-platform": "^5.0.0",
"@edx/frontend-platform": "^5.5.4",
"@edx/paragon": "20.46.2",
"@fortawesome/fontawesome-svg-core": "6.4.2",
"@fortawesome/free-brands-svg-icons": "6.4.2",

View File

@@ -4,6 +4,7 @@
<title>Authn | <%= process.env.SITE_NAME %></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.6/iframeResizer.contentWindow.min.js"
integrity="sha512-R7Piufj0/o6jG9ZKrAvS2dblFr2kkuG4XVQwStX+/4P+KwOLUXn2DXy0l1AJDxxqGhkM/FJllZHG2PKOAheYzg=="
crossorigin="anonymous"

View File

@@ -24,7 +24,7 @@ import { ForgotPasswordPage } from './forgot-password';
import Logistration from './logistration/Logistration';
import { ProgressiveProfiling } from './progressive-profiling';
import { RecommendationsPage } from './recommendations';
import { EmbeddableRegistrationPage } from './register';
import { RegistrationPage } from './register';
import { ResetPasswordPage } from './reset-password';
import './index.scss';
@@ -41,7 +41,7 @@ const MainApp = () => (
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
<Route
path={REGISTER_EMBEDDED_PAGE}
element={<EmbeddedRegistrationRoute><EmbeddableRegistrationPage /></EmbeddedRegistrationRoute>}
element={<EmbeddedRegistrationRoute><RegistrationPage /></EmbeddedRegistrationRoute>}
/>
<Route
path={LOGIN_PAGE}

View File

@@ -23,13 +23,15 @@ const MediumLayout = () => {
<div>
<h1
className={classNames(
'display-1 text-white mt-5 mb-5 mr-2',
'display-1 text-white mt-5 mb-5 mr-2 main-heading',
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' },
)}
>
<span className="mr-2">{formatMessage(messages['start.learning'])}</span>
<span className="text-accent-a d-inline-block">
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
<span>
{formatMessage(messages['start.learning'])}{' '}
<span className="text-accent-a d-inline-block">
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
</span>
</span>
</h1>
</div>

View File

@@ -17,17 +17,18 @@ const SmallLayout = () => {
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<div className="d-flex align-items-center mb-3 mt-3 mr-3">
<div className="d-flex align-items-center m-3.5">
<div className={classNames({ 'small-yellow-line mr-n2.5': getConfig().SITE_NAME === 'edX' })} />
<h1
className={classNames(
'text-white mt-3.5 mb-3.5',
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' },
)}
>
<span className="mr-1">{formatMessage(messages['start.learning'])}</span>
<span className="text-accent-a d-inline-block">
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
<span>
{formatMessage(messages['start.learning'])}{' '}
<span className="text-accent-a d-inline-block">
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
</span>
</span>
</h1>
</div>

View File

@@ -16,7 +16,7 @@ const SmallLayout = ({ username }) => {
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
</Hyperlink>
<div className="d-flex align-items-center mb-3 mt-3 mr-3">
<div className="d-flex align-items-center m-3.5">
<div className="small-yellow-line mt-4.5" />
<div>
<h1 className="h5 data-hj-suppress">

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { Navigate } from 'react-router-dom';
import {
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS,
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
} from '../data/constants';
import { setCookie } from '../data/utils';
@@ -18,6 +18,8 @@ const RedirectLogistration = (props) => {
redirectToRecommendationsPage,
educationLevel,
userId,
registrationEmbedded,
host,
} = props;
let finalRedirectUrl = '';
@@ -37,6 +39,13 @@ const RedirectLogistration = (props) => {
// TODO: Do we still need this cookie?
setCookie('van-504-returning-user', true);
if (registrationEmbedded) {
window.parent.postMessage({
action: REDIRECT,
redirectUrl: getConfig().POST_REGISTRATION_REDIRECT_URL,
}, host);
return null;
}
const registrationResult = { redirectUrl: finalRedirectUrl, success };
return (
<Navigate
@@ -83,6 +92,8 @@ RedirectLogistration.defaultProps = {
optionalFields: {},
redirectToRecommendationsPage: false,
userId: null,
registrationEmbedded: false,
host: '',
};
RedirectLogistration.propTypes = {
@@ -95,6 +106,8 @@ RedirectLogistration.propTypes = {
optionalFields: PropTypes.shape({}),
redirectToRecommendationsPage: PropTypes.bool,
userId: PropTypes.number,
registrationEmbedded: PropTypes.bool,
host: PropTypes.string,
};
export default RedirectLogistration;

View File

@@ -1,7 +1,7 @@
const configuration = {
// Cookies related configs
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN,
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME || null,
USER_RETENTION_COOKIE_NAME: process.env.USER_RETENTION_COOKIE_NAME || '',
// Features
DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '',
ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,

View File

@@ -71,7 +71,7 @@
"non.compliant.password.message": "كلمة مرورك الحالية لا تستسجيب لمتطلبات الأمان الجديدة. لقد أرسلنا للتو رسالة لإعادة ضبط كلمة المرور إلى عنوان البريد الإلكتروني المرتبط بهذا الحساب. شكرًا لك على مساعدتنا في الحفاظ على سلامة بياناتك.",
"account.locked.out.message.1": "لحماية حسابك، تم إقفاله مؤقتًا. حاول مرة أخرى بعد 30 دقيقة.",
"enterprise.login.btn.text": "بيانات الشركة أو المدرسة",
"username.or.email.format.validation.less.chars.message": "يجب أن يحتوي اسم المستخدم أو البريد الإلكتروني على 3 أحرف على الأقل.",
"username.or.email.format.validation.less.chars.message": "يجب أن يحتوي اسم المستخدم أو البريد الإلكتروني على 2 أحرف على الأقل.",
"email.validation.message": "أدخل اسم المستخدم أو البريد الإلكتروني الخاص بك",
"password.validation.message": "لم يتم استيفاء معايير كلمة المرور",
"account.activation.success.message.title": "نجح الأمر! لقد قمت بتفعيل حسابك.",
@@ -177,4 +177,4 @@
"reset.password.success.heading": "تمت إعادة ضبط كلمة المرور.",
"reset.password.success": "تمت إعادة ضبط كلمة مرورك. سجل الدخول إلى حسابك.",
"rate.limit.error": "حدث خطأ بسبب كثرة الطلبات. رجاءً حاول مرة أخرى بعد مضي بعض الوقت."
}
}

View File

@@ -71,7 +71,7 @@
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
"enterprise.login.btn.text": "Company or school credentials",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 2 characters.",
"email.validation.message": "Enter your username or email",
"password.validation.message": "Password criteria has not been met",
"account.activation.success.message.title": "Success! You have activated your account.",
@@ -177,4 +177,4 @@
"reset.password.success.heading": "Password reset complete.",
"reset.password.success": "Your password has been reset. Sign in to your account.",
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
}
}

View File

@@ -71,7 +71,7 @@
"non.compliant.password.message": "Ihr aktuelles Passwort entspricht nicht den neuen Sicherheitsanforderungen. Wir haben gerade eine Nachricht zum Zurücksetzen des Passworts an die mit diesem Konto verknüpfte E-Mail-Adresse gesendet. Vielen Dank, dass Sie uns helfen, Ihre Daten zu schützen.",
"account.locked.out.message.1": "Um Ihr Konto zu schützen, wurde es vorübergehend gesperrt. Versuchen Sie es in 30 Minuten erneut.",
"enterprise.login.btn.text": "Arbeits- oder Schulzeugnisse",
"username.or.email.format.validation.less.chars.message": "Benutzername oder E-Mail müssen mindestens 3 Zeichen lang sein.",
"username.or.email.format.validation.less.chars.message": "Benutzername oder E-Mail müssen mindestens 2 Zeichen lang sein.",
"email.validation.message": "Geben Sie Ihren Benutzernamen oder Ihre E-Mail-Adresse ein",
"password.validation.message": "Die Passwortkriterien wurden nicht erfüllt",
"account.activation.success.message.title": "Super! Sie haben Ihr Konto aktiviert.",
@@ -177,4 +177,4 @@
"reset.password.success.heading": "Zurücksetzen des Passworts abgeschlossen.",
"reset.password.success": "Ihr Passwort wurde zurückgesetzt. Melden Sie sich bei Ihrem Konto an.",
"rate.limit.error": "Aufgrund zu vieler Anfragen ist ein Fehler aufgetreten. Bitte versuchen Sie es nach einiger Zeit erneut."
}
}

View File

@@ -71,7 +71,7 @@
"non.compliant.password.message": "Tu contraseña actual no cumple con los nuevos requisitos de seguridad. Acabamos de enviar un mensaje de restablecimiento de contraseña a la dirección de correo electrónico asociada a esta cuenta. Gracias por ayudarnos a mantener tus datos seguros.",
"account.locked.out.message.1": "Para proteger tu cuenta, se ha bloqueado temporalmente. Inténtalo de nuevo en 30 minutos.",
"enterprise.login.btn.text": "Credenciales de la empresa o de la institución ",
"username.or.email.format.validation.less.chars.message": "El nombre de usuario o el correo electrónico deben tener al menos 3 caracteres.",
"username.or.email.format.validation.less.chars.message": "El nombre de usuario o el correo electrónico deben tener al menos 2 caracteres.",
"email.validation.message": "Introduce tu nombre de usuario o correo electrónico",
"password.validation.message": "No se han cumplido los criterios de la contraseña",
"account.activation.success.message.title": "Ha sido un éxito. Has activado tu cuenta.",
@@ -177,4 +177,4 @@
"reset.password.success.heading": "Restablecimiento de la contraseña completado.",
"reset.password.success": "Tu contraseña ha sido restablecida. Acceda a tu cuenta.",
"rate.limit.error": "Se ha producido un error debido a demasiadas solicitudes. Por favor, inténtalo de nuevo después de algún tiempo."
}
}

View File

@@ -71,7 +71,7 @@
"non.compliant.password.message": "گذرواژه فعلی شما الزامات امنیتی جدید را برآورده نمی‌کند. ما فقط یک پیام بازتنظیم گذرواژه به نشانی رایانامه مرتبط با این حساب کاربری ارسال کردیم. از اینکه به ما کمک می‌کنید تا داده‌های شما را ایمن نگه دارید متشکریم.",
"account.locked.out.message.1": "حساب کاربری شما، به دلیل حفاظت، به‌طور موقت قفل شده است. 30 دقیقه دیگر دوباره امتحان کنید.",
"enterprise.login.btn.text": "اعتبار دانشکده یا شرکت",
"username.or.email.format.validation.less.chars.message": "نام کاربری یا نشانی رایانامه حداقل باید 3 نویسه داشته باشد",
"username.or.email.format.validation.less.chars.message": "نام کاربری یا نشانی رایانامه حداقل باید 2 نویسه داشته باشد",
"email.validation.message": "نام کاربری یا رایانامه خود را وارد کنید",
"password.validation.message": "معیارهای گذرواژه رعایت نشده است",
"account.activation.success.message.title": "موفق شدید! شما حساب کاربری خود را فعال کردید.",
@@ -177,4 +177,4 @@
"reset.password.success.heading": "بازتنظیم گذرواژه  تکمیل شد.",
"reset.password.success": "گذرواژه شما بازتنظیم شد. وارد حساب کاربری خود شوید",
"rate.limit.error": "به دلیل درخواست‌های زیاد، خطایی روی داده است. لطفا بعد از مدتی دوباره امتحان کنید."
}
}

View File

@@ -71,7 +71,7 @@
"non.compliant.password.message": "Votre mot de passe actuel ne répond pas aux nouvelles exigences de sécurité. Nous venons d'envoyer un message de réinitialisation de mot de passe à l'adresse courriel associée à ce compte. Merci de nous aider à protéger vos données.",
"account.locked.out.message.1": "Pour protéger votre compte, il a été temporairement verrouillé. Réessayez dans 30 minutes.",
"enterprise.login.btn.text": "Identifiants de la compagnie ou de l'école",
"username.or.email.format.validation.less.chars.message": "Le nom d'utilisateur ou l'adresse courriel doit comporter au moins 3 caractères.",
"username.or.email.format.validation.less.chars.message": "Le nom d'utilisateur ou l'adresse courriel doit comporter au moins 2 caractères.",
"email.validation.message": "Entrez votre nom d'utilisateur ou votre adresse courriel",
"password.validation.message": "Les critères de mot de passe n'ont pas été remplis",
"account.activation.success.message.title": "Succès! Vous avez activé votre compte.",
@@ -177,4 +177,4 @@
"reset.password.success.heading": "Réinitialisation du mot de passe complétée.",
"reset.password.success": "Votre mot de passe a été réinitialisé. Connectez-vous à votre compte.",
"rate.limit.error": "Une erreur s'est produite en raison d'un trop grand nombre de demandes. Veuillez réessayer après un certain temps."
}
}

View File

@@ -71,7 +71,7 @@
"non.compliant.password.message": "Votre mot de passe actuel ne répond pas aux nouvelles exigences de sécurité. Nous venons d'envoyer un message de réinitialisation de mot de passe à l'adresse courriel associée à ce compte. Merci de nous aider à protéger vos données.",
"account.locked.out.message.1": "Pour protéger votre compte, il a été temporairement verrouillé. Réessayez dans 30 minutes.",
"enterprise.login.btn.text": "Informations d'identification de la compagnie ou de l'école",
"username.or.email.format.validation.less.chars.message": "Le nom d'utilisateur ou l'adresse courriel doit comporter au moins 3 caractères.",
"username.or.email.format.validation.less.chars.message": "Le nom d'utilisateur ou l'adresse courriel doit comporter au moins 2 caractères.",
"email.validation.message": "Entrez votre nom d'utilisateur ou votre adresse courriel",
"password.validation.message": "Les critères de mot de passe n'ont pas été remplis",
"account.activation.success.message.title": "Succès! Vous avez activé votre compte.",
@@ -177,4 +177,4 @@
"reset.password.success.heading": "Réinitialisation du mot de passe complétée.",
"reset.password.success": "Votre mot de passe a été réinitialisé. Connectez-vous à votre compte.",
"rate.limit.error": "Une erreur s'est produite en raison d'un trop grand nombre de demandes. Veuillez réessayer après un certain temps."
}
}

View File

@@ -71,7 +71,7 @@
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
"enterprise.login.btn.text": "Company or school credentials",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 2 characters.",
"email.validation.message": "Enter your username or email",
"password.validation.message": "Password criteria has not been met",
"account.activation.success.message.title": "Success! You have activated your account.",
@@ -177,4 +177,4 @@
"reset.password.success.heading": "Password reset complete.",
"reset.password.success": "Your password has been reset. Sign in to your account.",
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
}
}

View File

@@ -71,7 +71,7 @@
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
"enterprise.login.btn.text": "Company or school credentials",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 2 characters.",
"email.validation.message": "Enter your username or email",
"password.validation.message": "Password criteria has not been met",
"account.activation.success.message.title": "Success! You have activated your account.",
@@ -177,4 +177,4 @@
"reset.password.success.heading": "Password reset complete.",
"reset.password.success": "Your password has been reset. Sign in to your account.",
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
}
}

View File

@@ -71,7 +71,7 @@
"non.compliant.password.message": "La tua password attuale non soddisfa i nuovi requisiti di sicurezza. Abbiamo appena inviato un messaggio di reimpostazione della password all&#39;indirizzo e-mail associato a questo account. Grazie per averci aiutato a mantenere i tuoi dati al sicuro.",
"account.locked.out.message.1": "Per proteggere il tuo account, è stato temporaneamente bloccato. Riprova tra 30 minuti.",
"enterprise.login.btn.text": "Credenziali aziendali o scolastiche",
"username.or.email.format.validation.less.chars.message": "Il nome utente o l&#39;e-mail deve contenere almeno 3 caratteri.",
"username.or.email.format.validation.less.chars.message": "Il nome utente o l&#39;e-mail deve contenere almeno 2 caratteri.",
"email.validation.message": "Inserisci il tuo nome utente o e-mail",
"password.validation.message": "I criteri della password non sono stati soddisfatti",
"account.activation.success.message.title": "Completato correttamente! Hai attivato il tuo account. ",
@@ -177,4 +177,4 @@
"reset.password.success.heading": "Ripristino della password completato.",
"reset.password.success": "La tua password è stata resettata. Accedi al tuo account.",
"rate.limit.error": "Si è verificato un errore dovuto alle troppe richieste. Prova di nuovo più tardi."
}
}

View File

@@ -71,7 +71,7 @@
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
"enterprise.login.btn.text": "Company or school credentials",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 2 characters.",
"email.validation.message": "Enter your username or email",
"password.validation.message": "Password criteria has not been met",
"account.activation.success.message.title": "Success! You have activated your account.",
@@ -177,4 +177,4 @@
"reset.password.success.heading": "Password reset complete.",
"reset.password.success": "Your password has been reset. Sign in to your account.",
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
}
}

View File

@@ -71,7 +71,7 @@
"non.compliant.password.message": "A sua palavra-passe atual não satisfaz os novos requisitos de segurança. Acabámos de enviar uma mensagem de redefinição da palavra-passe para o endereço de email associado a esta conta. Obrigado por nos ajudar a manter os seus dados em segurança.",
"account.locked.out.message.1": "Para proteger sua conta, esta foi temporariamente bloqueada. Tente novamente dentro de 30 minutos.",
"enterprise.login.btn.text": "Credenciais da empresa ou escola",
"username.or.email.format.validation.less.chars.message": "O nome de utilizador ou email deve ter pelo menos 3 carateres.",
"username.or.email.format.validation.less.chars.message": "O nome de utilizador ou email deve ter pelo menos 2 carateres.",
"email.validation.message": "Insira o seu nome de utilizador ou email",
"password.validation.message": "Os critérios de palavra-passe não foram cumpridos",
"account.activation.success.message.title": "Sucesso! Você ativou a sua conta.",
@@ -177,4 +177,4 @@
"reset.password.success.heading": "Redefinição de palavra-passe concluída",
"reset.password.success": "A sua palavra-passe foi redefinida. Inicie sessão na sua conta.",
"rate.limit.error": "Ocorreu um erro devido a demasiados pedidos. Por favor, tente novamente após algum tempo."
}
}

View File

@@ -71,7 +71,7 @@
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
"enterprise.login.btn.text": "Company or school credentials",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 2 characters.",
"email.validation.message": "Enter your username or email",
"password.validation.message": "Password criteria has not been met",
"account.activation.success.message.title": "Success! You have activated your account.",
@@ -177,4 +177,4 @@
"reset.password.success.heading": "Password reset complete.",
"reset.password.success": "Your password has been reset. Sign in to your account.",
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
}
}

View File

@@ -71,7 +71,7 @@
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
"enterprise.login.btn.text": "Company or school credentials",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 2 characters.",
"email.validation.message": "Enter your username or email",
"password.validation.message": "Password criteria has not been met",
"account.activation.success.message.title": "Success! You have activated your account.",
@@ -177,4 +177,4 @@
"reset.password.success.heading": "Password reset complete.",
"reset.password.success": "Your password has been reset. Sign in to your account.",
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
}
}

View File

@@ -71,7 +71,7 @@
"non.compliant.password.message": "您当前的密码不符合新的安全要求。我们刚刚向与此帐户关联的电子邮件地址发送了密码重置邮件。感谢您帮助我们保护您的数据安全。",
"account.locked.out.message.1": "为了保护您的帐户,它已被暂时锁定。请在 30 分钟后重试。",
"enterprise.login.btn.text": "单位或学校证书",
"username.or.email.format.validation.less.chars.message": "用户名或电子邮件必须至少包含 3 个字符。",
"username.or.email.format.validation.less.chars.message": "用户名或电子邮件必须至少包含 2 个字符。",
"email.validation.message": "输入您的用户名或电子邮件",
"password.validation.message": "未满足密码条件",
"account.activation.success.message.title": "成功!您已激活您的帐户。",
@@ -177,4 +177,4 @@
"reset.password.success.heading": "密码重置完成。",
"reset.password.success": "您的密码已重置。登录到您的帐户。",
"rate.limit.error": "由于请求过多而发生错误。请稍后重试。"
}
}

View File

@@ -147,7 +147,7 @@ class LoginPage extends React.Component {
if (email === '') {
errors.emailOrUsername = this.props.intl.formatMessage(messages['email.validation.message']);
} else if (email.length < 3) {
} else if (email.length < 2) {
errors.emailOrUsername = this.props.intl.formatMessage(messages['username.or.email.format.validation.less.chars.message']);
} else {
errors.emailOrUsername = '';

View File

@@ -71,8 +71,8 @@ const messages = defineMessages({
},
'username.or.email.format.validation.less.chars.message': {
id: 'username.or.email.format.validation.less.chars.message',
defaultMessage: 'Username or email must have at least 3 characters.',
description: 'Validation message that appears when username or email address is less than 3 characters',
defaultMessage: 'Username or email must have at least 2 characters.',
description: 'Validation message that appears when username or email address is less than 2 characters',
},
'email.validation.message': {
id: 'email.validation.message',

View File

@@ -128,14 +128,14 @@ describe('LoginPage', () => {
expect(store.dispatch).toHaveBeenCalledWith(loginRequestFailure({ errorCode: 'invalid-form' }));
});
it('should match state for invalid email (less than 3 characters), on form submission', () => {
const errorState = { emailOrUsername: 'Username or email must have at least 3 characters.', password: '' };
it('should match state for invalid email (less than 2 characters), on form submission', () => {
const errorState = { emailOrUsername: 'Username or email must have at least 2 characters.', password: '' };
store.dispatch = jest.fn(store.dispatch);
const loginPage = (mount(reduxWrapper(<IntlLoginPage {...props} />))).find('LoginPage');
loginPage.find('input#password').simulate('change', { target: { value: 'test', name: 'password' } });
loginPage.find('input#emailOrUsername').simulate('change', { target: { value: 'te', name: 'email' } });
loginPage.find('input#emailOrUsername').simulate('change', { target: { value: 't', name: 'email' } });
loginPage.find('button.btn-brand').simulate('click');
expect(loginPage.state('errors')).toEqual(errorState);

View File

@@ -39,8 +39,8 @@ const BaseCard = ({
/>
<Card.Header
className="mt-2"
title={truncateText(title)}
subtitle={truncateText(subtitle)}
title={truncateText(title, 50)}
subtitle={truncateText(subtitle, 30)}
/>
<Card.Section className="d-flex">
<div className="product-badge">

View File

@@ -47,7 +47,9 @@ export const getVariant = (productType) => (
export const createCodeFriendlyProduct = (type) => type?.replace(/\s+/g, '-').replace(/'/g, '').toLowerCase();
export const truncateText = (input) => (input?.length > 50 ? `${input.substring(0, 50)}...` : input);
export const truncateText = (input, textLength) => (
input?.length > textLength ? `${input.substring(0, textLength)}...` : input
);
export const filterLocationRestriction = (products, countryCode) => products.filter((product) => {
if (product.locationRestriction) {

View File

@@ -7,6 +7,7 @@ import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Form, Spinner, StatefulButton } from '@edx/paragon';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import Skeleton from 'react-loading-skeleton';
@@ -36,7 +37,7 @@ import {
COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
} from '../data/constants';
import {
getAllPossibleQueryParams, getTpaHint, getTpaProvider, setCookie,
getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, setCookie,
} from '../data/utils';
/**
@@ -46,6 +47,7 @@ const RegistrationPage = (props) => {
const { formatMessage } = useIntl();
const dispatch = useDispatch();
const registrationEmbedded = isHostAvailableInQueryParams();
const platformName = getConfig().SITE_NAME;
const flags = {
showConfigurableEdxFields: getConfig().SHOW_CONFIGURABLE_EDX_FIELDS,
@@ -97,6 +99,13 @@ const RegistrationPage = (props) => {
const [errors, setErrors] = useState({ ...backedUpFormData.errors });
const [errorCode, setErrorCode] = useState({ type: '', count: 0 });
const [formStartTime, setFormStartTime] = useState(null);
// temporary error state for embedded experience because we don't want to show errors on blur
const [temporaryErrors, setTemporaryErrors] = useState({ ...backedUpFormData.errors });
const { cta, host } = queryParams;
const buttonLabel = cta
? formatMessage(messages['create.account.cta.button'], { label: cta })
: formatMessage(messages['create.account.for.free.button']);
/**
* Set the userPipelineDetails data in formFields for only first time
@@ -147,9 +156,13 @@ const RegistrationPage = (props) => {
useEffect(() => {
if (backendValidations) {
setErrors(prevErrors => ({ ...prevErrors, ...backendValidations }));
if (registrationEmbedded) {
setTemporaryErrors(prevErrors => ({ ...prevErrors, ...backendValidations }));
} else {
setErrors(prevErrors => ({ ...prevErrors, ...backendValidations }));
}
}
}, [backendValidations]);
}, [backendValidations, registrationEmbedded]);
useEffect(() => {
if (registrationErrorCode) {
@@ -159,26 +172,11 @@ const RegistrationPage = (props) => {
useEffect(() => {
if (registrationResult.success) {
// This event is used by GTM
sendTrackEvent('edx.bi.user.account.registered.client', {});
// Optimizely registration conversion event
window.optimizely = window.optimizely || [];
window.optimizely.push({
type: 'event',
eventName: 'authn-registration-conversion',
});
// We probably don't need this cookie because this fires the same event as
// above for optimizely using GTM.
setCookie(getConfig().REGISTER_CONVERSION_COOKIE_NAME, true);
// This is used by the "User Retention Rate Event" on GTM
setCookie('authn-returning-user');
// Fire GTM event used for integration with impact.com
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'ImpactRegistrationEvent',
});
setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true);
}
}, [registrationResult]);
@@ -193,10 +191,23 @@ const RegistrationPage = (props) => {
};
const handleErrorChange = (fieldName, error) => {
setErrors(prevErrors => ({
...prevErrors,
[fieldName]: error,
}));
if (registrationEmbedded) {
setTemporaryErrors(prevErrors => ({
...prevErrors,
[fieldName]: error,
}));
if (error === '' && errors[fieldName] !== '') {
setErrors(prevErrors => ({
...prevErrors,
[fieldName]: error,
}));
}
} else {
setErrors(prevErrors => ({
...prevErrors,
[fieldName]: error,
}));
}
};
const registerUser = () => {
@@ -211,7 +222,7 @@ const RegistrationPage = (props) => {
// Validating form data before submitting
const { isValid, fieldErrors } = isFormValid(
payload,
errors,
registrationEmbedded ? temporaryErrors : errors,
configurableFormFields,
fieldDescriptions,
formatMessage,
@@ -262,11 +273,13 @@ const RegistrationPage = (props) => {
<title>{formatMessage(messages['register.page.title'], { siteName: getConfig().SITE_NAME })}</title>
</Helmet>
<RedirectLogistration
host={host}
authenticatedUser={registrationResult.authenticatedUser}
success={registrationResult.success}
redirectUrl={registrationResult.redirectUrl}
finishAuthUrl={finishAuthUrl}
optionalFields={optionalFields}
registrationEmbedded={registrationEmbedded}
redirectToProgressiveProfilingPage={
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && !!Object.keys(optionalFields.fields).length
}
@@ -276,7 +289,12 @@ const RegistrationPage = (props) => {
<Spinner animation="border" variant="primary" id="tpa-spinner" />
</div>
) : (
<div className="mw-xs mt-3">
<div
className={classNames(
'mw-xs mt-3',
{ 'w-100 m-auto pt-4 main-content': registrationEmbedded },
)}
>
<ThirdPartyAuthAlert
currentProvider={currentProvider}
platformName={platformName}
@@ -332,7 +350,7 @@ const RegistrationPage = (props) => {
email={formFields.email}
fieldErrors={errors}
formFields={configurableFormFields}
setFieldErrors={setErrors}
setFieldErrors={registrationEmbedded ? setTemporaryErrors : setErrors}
setFormFields={setConfigurableFormFields}
autoSubmitRegisterForm={autoSubmitRegForm}
fieldDescriptions={fieldDescriptions}
@@ -345,19 +363,21 @@ const RegistrationPage = (props) => {
className="register-button mt-4 mb-4"
state={submitState}
labels={{
default: formatMessage(messages['create.account.for.free.button']),
default: buttonLabel,
pending: '',
}}
onClick={handleSubmit}
onMouseDown={(e) => e.preventDefault()}
/>
<ThirdPartyAuth
currentProvider={currentProvider}
providers={providers}
secondaryProviders={secondaryProviders}
handleInstitutionLogin={handleInstitutionLogin}
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
/>
{!registrationEmbedded && (
<ThirdPartyAuth
currentProvider={currentProvider}
providers={providers}
secondaryProviders={secondaryProviders}
handleInstitutionLogin={handleInstitutionLogin}
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
/>
)}
</Form>
</div>
)}

View File

@@ -59,7 +59,7 @@ describe('RegistrationPage', () => {
mergeConfig({
PRIVACY_POLICY: 'https://privacy-policy.com',
TOS_AND_HONOR_CODE: 'https://tos-and-honot-code.com',
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME,
USER_RETENTION_COOKIE_NAME: 'authn-returning-user',
});
let props = {};
@@ -503,6 +503,14 @@ describe('RegistrationPage', () => {
expect(registrationPage.find('.institutions__heading').text()).toEqual('Register with institution/campus credentials');
});
it('should show button label based on cta query params value', () => {
const buttonLabel = 'Register';
delete window.location;
window.location = { href: getConfig().BASE_URL, search: `?cta=${buttonLabel}` };
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
expect(registrationPage.find('button[type="submit"] span').first().text()).toEqual(buttonLabel);
});
it('should not display password field when current provider is present', () => {
store = mockStore({
...initialState,
@@ -519,7 +527,7 @@ describe('RegistrationPage', () => {
expect(registrationPage.find('input#password').length).toEqual(0);
});
it('should check registration conversion cookie', () => {
it('should check user retention cookie', () => {
store = mockStore({
...initialState,
register: {
@@ -531,7 +539,7 @@ describe('RegistrationPage', () => {
});
renderer.create(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(document.cookie).toMatch(`${getConfig().REGISTER_CONVERSION_COOKIE_NAME}=true`);
expect(document.cookie).toMatch(`${getConfig().USER_RETENTION_COOKIE_NAME}=true`);
});
it('should redirect to url returned in registration result after successful account creation', () => {
@@ -884,6 +892,96 @@ describe('RegistrationPage', () => {
expect(registrationPage.find('.email-suggestion-alert-warning').first().text()).toEqual('john.doe@hotmail.com');
});
// ********* Embedded experience tests *********/
it('should call the postMessage API when embedded variant is rendered', () => {
getLocale.mockImplementation(() => ('en-us'));
mergeConfig({
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: true,
});
window.parent.postMessage = jest.fn();
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING), search: '?host=http://localhost/host-website' };
store = mockStore({
...initialState,
register: {
...initialState.register,
registrationResult: {
success: true,
},
},
commonComponents: {
...initialState.commonComponents,
optionalFields: {
extended_profile: {},
fields: {
level_of_education: { name: 'level_of_education', error_message: false },
},
},
},
});
const progressiveProfilingPage = mount(reduxWrapper(
<IntlRegistrationPage {...props} />,
));
progressiveProfilingPage.update();
expect(window.parent.postMessage).toHaveBeenCalledTimes(2);
});
it('should not display validations error on blur event when embedded variant is rendered', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: '?host=http://localhost/host-website' };
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registrationPage.find('input#username').simulate('blur', { target: { value: '', name: 'username' } });
expect(registrationPage.find('div[feedback-for="username"]').exists()).toBeFalsy();
registrationPage.find('input[name="country"]').simulate('blur', { target: { value: '', name: 'country' } });
expect(registrationPage.find('div[feedback-for="country"]').exists()).toBeFalsy();
});
it('should set errors in temporary state when validations are returned by registration api', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: '?host=http://localhost/host-website' };
const usernameError = 'It looks like this username is already taken';
const emailError = 'This email is already associated with an existing or previous account';
store = mockStore({
...initialState,
register: {
...initialState.register,
registrationError: {
username: [{ userMessage: usernameError }],
email: [{ userMessage: emailError }],
},
},
});
const registrationPage = mount(routerWrapper(reduxWrapper(
<IntlRegistrationPage {...props} />),
)).find('RegistrationPage');
expect(registrationPage.find('div[feedback-for="username"]').exists()).toBeFalsy();
expect(registrationPage.find('div[feedback-for="email"]').exists()).toBeFalsy();
});
it('should clear error on focus for embedded experience also', () => {
delete window.location;
window.location = {
href: getConfig().BASE_URL.concat(REGISTER_PAGE),
search: '?host=http://localhost/host-website',
};
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
registrationPage.find('button.btn-brand').simulate('click');
expect(registrationPage.find('div[feedback-for="password"]').text()).toContain(emptyFieldValidation.password);
registrationPage.find('input#password').simulate('focus');
expect(registrationPage.find('div[feedback-for="password"]').exists()).toBeFalsy();
});
it('should show spinner instead of form while registering if autoSubmitRegForm is true', () => {
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
getLocale.mockImplementation(() => ('en-us'));

View File

@@ -9,8 +9,8 @@ import { mount } from 'enzyme';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import { FIELDS } from '../../data/constants';
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
import ConfigurableRegistrationForm from './ConfigurableRegistrationForm';
import { FIELDS } from '../data/constants';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
@@ -44,7 +44,6 @@ describe('ConfigurableRegistrationForm', () => {
mergeConfig({
PRIVACY_POLICY: 'https://privacy-policy.com',
TOS_AND_HONOR_CODE: 'https://tos-and-honot-code.com',
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME,
});
let props = {};

View File

@@ -1,261 +0,0 @@
import React, {
useEffect, useMemo, useState,
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Form, StatefulButton } from '@edx/paragon';
import { Helmet } from 'react-helmet';
import ConfigurableRegistrationForm from './ConfigurableRegistrationForm';
import RegistrationFailure from './RegistrationFailure';
import { PasswordField } from '../../common-components';
import { getThirdPartyAuthContext as getRegistrationDataFromBackend } from '../../common-components/data/actions';
import { REDIRECT } from '../../data/constants';
import { getAllPossibleQueryParams, setCookie } from '../../data/utils';
import { clearRegistrationBackendError, registerNewUser } from '../data/actions';
import { FORM_SUBMISSION_ERROR } from '../data/constants';
import { getBackendValidations, isFormValid, prepareRegistrationPayload } from '../data/utils';
import messages from '../messages';
import { EmailField, NameField, UsernameField } from '../RegistrationFields';
/**
* Main Registration Page component
*/
const EmbeddableRegistrationPage = () => {
const { formatMessage } = useIntl();
const dispatch = useDispatch();
const flags = {
showConfigurableEdxFields: getConfig().SHOW_CONFIGURABLE_EDX_FIELDS,
showConfigurableRegistrationFields: getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS,
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
};
const {
registrationFormData: backedUpFormData,
registrationError,
registrationError: {
errorCode: registrationErrorCode,
} = {},
registrationResult,
submitState,
validations,
} = useSelector(state => state.register);
const { fieldDescriptions } = useSelector(state => state.commonComponents);
const backendValidations = useMemo(
() => getBackendValidations(registrationError, validations), [registrationError, validations],
);
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
const [formFields, setFormFields] = useState({ ...backedUpFormData.formFields });
const [configurableFormFields, setConfigurableFormFields] = useState(
{ ...backedUpFormData.configurableFormFields },
);
const [errors, setErrors] = useState({ ...backedUpFormData.errors });
const [errorCode, setErrorCode] = useState({ type: '', count: 0 });
const [formStartTime, setFormStartTime] = useState(null);
// temporary error state for embedded experience because we don't want to show errors on blur
const [temporaryErrors, setTemporaryErrors] = useState({ ...backedUpFormData.errors });
const { cta, host } = queryParams;
const buttonLabel = cta
? formatMessage(messages['create.account.cta.button'], { label: cta })
: formatMessage(messages['create.account.for.free.button']);
useEffect(() => {
if (!formStartTime) {
sendPageEvent('login_and_registration', 'register');
const payload = { ...queryParams, is_register_page: true };
dispatch(getRegistrationDataFromBackend(payload));
setFormStartTime(Date.now());
}
}, [dispatch, formStartTime, queryParams]);
useEffect(() => {
if (backendValidations) {
setTemporaryErrors(prevErrors => ({ ...prevErrors, ...backendValidations }));
}
}, [backendValidations]);
useEffect(() => {
if (registrationErrorCode) {
setErrorCode(prevState => ({ type: registrationErrorCode, count: prevState.count + 1 }));
}
}, [registrationErrorCode]);
useEffect(() => {
if (registrationResult.success) {
sendTrackEvent('edx.bi.user.account.registered.client', {});
// Optimizely registration conversion event
window.optimizely = window.optimizely || [];
window.optimizely.push({
type: 'event',
eventName: 'authn-registration-conversion',
});
// We probably don't need this cookie because this fires the same event as
// above for optimizely using GTM.
setCookie(getConfig().REGISTER_CONVERSION_COOKIE_NAME, true);
// This is used by the "User Retention Rate Event" on GTM
setCookie('authn-returning-user');
// Fire GTM event used for integration with impact.com
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'ImpactRegistrationEvent',
});
window.parent.postMessage({
action: REDIRECT,
redirectUrl: encodeURIComponent(getConfig().POST_REGISTRATION_REDIRECT_URL),
}, host);
}
}, [host, registrationResult]);
const handleOnChange = (event) => {
const { name } = event.target;
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
if (registrationError[name]) {
dispatch(clearRegistrationBackendError(name));
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
}
setFormFields(prevState => ({ ...prevState, [name]: value }));
};
const handleErrorChange = (fieldName, error) => {
setTemporaryErrors(prevErrors => ({
...prevErrors,
[fieldName]: error,
}));
if (error === '' && errors[fieldName] !== '') {
setErrors(prevErrors => ({
...prevErrors,
[fieldName]: error,
}));
}
};
const registerUser = () => {
const totalRegistrationTime = (Date.now() - formStartTime) / 1000;
let payload = { ...formFields };
// Validating form data before submitting
const { isValid, fieldErrors } = isFormValid(
payload,
temporaryErrors,
configurableFormFields,
fieldDescriptions,
formatMessage,
);
setErrors({ ...fieldErrors });
// returning if not valid
if (!isValid) {
setErrorCode(prevState => ({ type: FORM_SUBMISSION_ERROR, count: prevState.count + 1 }));
return;
}
// Preparing payload for submission
payload = prepareRegistrationPayload(
payload,
configurableFormFields,
flags.showMarketingEmailOptInCheckbox,
totalRegistrationTime,
queryParams);
// making register call
dispatch(registerNewUser(payload));
};
const handleSubmit = (e) => {
e.preventDefault();
registerUser();
};
return (
<>
<Helmet>
<title>{formatMessage(messages['register.page.title'], { siteName: getConfig().SITE_NAME })}</title>
</Helmet>
<div
className="mw-xs mt-3 w-100 m-auto pt-4 main-content"
>
<RegistrationFailure
errorCode={errorCode.type}
failureCount={errorCode.count}
/>
<Form id="registration-form" name="registration-form">
<NameField
name="name"
value={formFields.name}
shouldFetchUsernameSuggestions={!formFields.username.trim()}
handleChange={handleOnChange}
handleErrorChange={handleErrorChange}
errorMessage={errors.name}
helpText={[formatMessage(messages['help.text.name'])]}
floatingLabel={formatMessage(messages['registration.fullname.label'])}
/>
<EmailField
name="email"
value={formFields.email}
confirmEmailValue={configurableFormFields?.confirm_email}
handleErrorChange={handleErrorChange}
handleChange={handleOnChange}
errorMessage={errors.email}
helpText={[formatMessage(messages['help.text.email'])]}
floatingLabel={formatMessage(messages['registration.email.label'])}
/>
<UsernameField
name="username"
spellCheck="false"
value={formFields.username}
handleChange={handleOnChange}
handleErrorChange={handleErrorChange}
errorMessage={errors.username}
helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]}
floatingLabel={formatMessage(messages['registration.username.label'])}
/>
<PasswordField
name="password"
value={formFields.password}
handleChange={handleOnChange}
handleErrorChange={handleErrorChange}
errorMessage={errors.password}
floatingLabel={formatMessage(messages['registration.password.label'])}
/>
<ConfigurableRegistrationForm
email={formFields.email}
fieldErrors={errors}
formFields={configurableFormFields}
setFieldErrors={setTemporaryErrors}
setFormFields={setConfigurableFormFields}
autoSubmitRegisterForm={false}
fieldDescriptions={fieldDescriptions}
/>
<StatefulButton
id="register-user"
name="register-user"
type="submit"
variant="brand"
className="register-button mt-4 mb-4"
state={submitState}
labels={{
default: buttonLabel,
pending: '',
}}
onClick={handleSubmit}
onMouseDown={(e) => e.preventDefault()}
/>
</Form>
</div>
</>
);
};
export default EmbeddableRegistrationPage;

View File

@@ -1,672 +0,0 @@
import React from 'react';
import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
configure, getLocale, injectIntl, IntlProvider,
} from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme';
import { BrowserRouter as Router } from 'react-router-dom';
import renderer from 'react-test-renderer';
import configureStore from 'redux-mock-store';
import {
AUTHN_PROGRESSIVE_PROFILING, PENDING_STATE, REGISTER_PAGE,
} from '../../../data/constants';
import {
clearRegistrationBackendError,
registerNewUser,
} from '../../data/actions';
import {
FIELDS,
FORBIDDEN_REQUEST,
INTERNAL_SERVER_ERROR,
TPA_AUTHENTICATION_FAILURE,
TPA_SESSION_EXPIRED,
} from '../../data/constants';
import { EmbeddableRegistrationPage } from '../../index';
import RegistrationFailureMessage from '../RegistrationFailure';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(),
}));
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
const IntlEmbeddableRegistrationPage = injectIntl(EmbeddableRegistrationPage);
const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage);
const mockStore = configureStore();
jest.mock('react-router-dom', () => {
const mockNavigation = jest.fn();
// eslint-disable-next-line react/prop-types
const Navigate = ({ to }) => {
mockNavigation(to);
return <div />;
};
return {
...jest.requireActual('react-router-dom'),
Navigate,
mockNavigate: mockNavigation,
};
});
describe('EmbeddableRegistrationPage', () => {
mergeConfig({
PRIVACY_POLICY: 'https://privacy-policy.com',
TOS_AND_HONOR_CODE: 'https://tos-and-honot-code.com',
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME,
});
let props = {};
let store = {};
const registrationFormData = {
configurableFormFields: {
marketingEmailsOptIn: true,
},
formFields: {
name: '', email: '', username: '', password: '',
},
emailSuggestion: {
suggestion: '', type: '',
},
errors: {
name: '', email: '', username: '', password: '',
},
};
const reduxWrapper = children => (
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
);
const routerWrapper = children => (
<Router>
{children}
</Router>
);
const thirdPartyAuthContext = {
currentProvider: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
countryCode: null,
};
const initialState = {
register: {
registrationResult: { success: false, redirectUrl: '' },
registrationError: {},
registrationFormData,
usernameSuggestions: [],
},
commonComponents: {
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext,
fieldDescriptions: {},
optionalFields: {
fields: {},
extended_profile: [],
},
},
};
beforeEach(() => {
store = mockStore(initialState);
configure({
loggingService: { logError: jest.fn() },
config: {
ENVIRONMENT: 'production',
LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum',
},
messages: { 'es-419': {}, de: {}, 'en-us': {} },
});
props = {};
window.location = { search: '' };
window.parent.postMessage = jest.fn();
getLocale.mockImplementation(() => ('en-us'));
});
afterEach(() => {
jest.clearAllMocks();
});
const populateRequiredFields = (registrationPage, payload, isThirdPartyAuth = false) => {
registrationPage.find('input#name').simulate('change', { target: { value: payload.name, name: 'name' } });
registrationPage.find('input#username').simulate('change', { target: { value: payload.username, name: 'username' } });
registrationPage.find('input#email').simulate('change', { target: { value: payload.email, name: 'email' } });
registrationPage.find('input[name="country"]').simulate('change', { target: { value: payload.country, name: 'country' } });
registrationPage.find('input[name="country"]').simulate('blur', { target: { value: payload.country, name: 'country' } });
if (!isThirdPartyAuth) {
registrationPage.find('input#password').simulate('change', { target: { value: payload.password, name: 'password' } });
}
};
describe('Test Embeddable Registration Page', () => {
mergeConfig({
SHOW_CONFIGURABLE_EDX_FIELDS: true,
});
const emptyFieldValidation = {
name: 'Enter your full name',
username: 'Username must be between 2 and 30 characters',
email: 'Enter your email',
password: 'Password criteria has not been met',
country: 'Select your country or region of residence',
};
// ******** test registration form submission ********
it('should submit form for valid input', () => {
getLocale.mockImplementation(() => ('en-us'));
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
delete window.location;
window.location = { href: getConfig().BASE_URL, search: '?next=/course/demo-course-url' };
const payload = {
name: 'John Doe',
username: 'john_doe',
email: 'john.doe@gmail.com',
password: 'password1',
country: 'Pakistan',
honor_code: true,
totalRegistrationTime: 0,
next: '/course/demo-course-url',
};
store.dispatch = jest.fn(store.dispatch);
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />)));
populateRequiredFields(registrationPage, payload);
registrationPage.find('button.btn-brand').simulate('click');
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
});
it('should submit form with marketing email opt in value', () => {
mergeConfig({
MARKETING_EMAILS_OPT_IN: 'true',
});
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
const payload = {
name: 'John Doe',
username: 'john_doe',
email: 'john.doe@gmail.com',
password: 'password1',
country: 'Pakistan',
honor_code: true,
totalRegistrationTime: 0,
marketing_emails_opt_in: true,
};
store.dispatch = jest.fn(store.dispatch);
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />)));
populateRequiredFields(registrationPage, payload);
registrationPage.find('button.btn-brand').simulate('click');
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
mergeConfig({
MARKETING_EMAILS_OPT_IN: '',
});
});
it('should not dispatch registerNewUser on empty form Submission', () => {
store.dispatch = jest.fn(store.dispatch);
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />)));
registrationPage.find('button.btn-brand').simulate('click');
expect(store.dispatch).not.toHaveBeenCalledWith(registerNewUser({}));
});
// ******** test registration form validations ********
it('should show error messages for required fields on empty form submission', () => {
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />)));
registrationPage.find('button.btn-brand').simulate('click');
expect(registrationPage.find('div[feedback-for="name"]').text()).toEqual(emptyFieldValidation.name);
expect(registrationPage.find('div[feedback-for="username"]').text()).toEqual(emptyFieldValidation.username);
expect(registrationPage.find('div[feedback-for="email"]').text()).toEqual(emptyFieldValidation.email);
expect(registrationPage.find('div[feedback-for="password"]').text()).toContain(emptyFieldValidation.password);
expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(emptyFieldValidation.country);
const alertBanner = 'We couldn\'t create your account.Please check your responses and try again.';
expect(registrationPage.find('#validation-errors').first().text()).toEqual(alertBanner);
});
it('should clear error on focus', () => {
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />)));
registrationPage.find('button.btn-brand').simulate('click');
expect(registrationPage.find('div[feedback-for="password"]').text()).toContain(emptyFieldValidation.password);
registrationPage.find('input#password').simulate('focus');
expect(registrationPage.find('div[feedback-for="password"]').exists()).toBeFalsy();
});
it('should clear registration backend error on change', () => {
getLocale.mockImplementation(() => ('en-us'));
const emailError = 'This email is already associated with an existing or previous account';
store = mockStore({
...initialState,
register: {
...initialState.register,
registrationError: {
email: [{ userMessage: emailError }],
},
},
});
store.dispatch = jest.fn(store.dispatch);
const registrationPage = mount(routerWrapper(reduxWrapper(
<IntlEmbeddableRegistrationPage {...props} />,
))).find('EmbeddableRegistrationPage');
registrationPage.find('input#email').simulate('change', { target: { value: 'test1@gmail.com', name: 'email' } });
expect(store.dispatch).toHaveBeenCalledWith(clearRegistrationBackendError('email'));
});
// ******** test alert messages ********
it('should match internal server error message', () => {
const expectedMessage = 'We couldn\'t create your account.An error has occurred. Try refreshing the page, or check your internet connection.';
props = {
errorCode: INTERNAL_SERVER_ERROR,
failureCount: 0,
};
const registrationPage = mount(reduxWrapper(<IntlRegistrationFailure {...props} />));
expect(registrationPage.find('div.alert-heading').length).toEqual(1);
expect(registrationPage.find('div.alert').first().text()).toEqual(expectedMessage);
});
it('should match registration api rate limit error message', () => {
const expectedMessage = 'We couldn\'t create your account.Too many failed registration attempts. Try again later.';
props = {
errorCode: FORBIDDEN_REQUEST,
failureCount: 0,
};
const registrationPage = mount(reduxWrapper(<IntlRegistrationFailure {...props} />));
expect(registrationPage.find('div.alert-heading').length).toEqual(1);
expect(registrationPage.find('div.alert').first().text()).toEqual(expectedMessage);
});
it('should match tpa session expired error message', () => {
const expectedMessage = 'We couldn\'t create your account.Registration using Google has timed out.';
props = {
context: {
provider: 'Google',
},
errorCode: TPA_SESSION_EXPIRED,
failureCount: 0,
};
const registrationPage = mount(reduxWrapper(<IntlRegistrationFailure {...props} />));
expect(registrationPage.find('div.alert-heading').length).toEqual(1);
expect(registrationPage.find('div.alert').first().text()).toEqual(expectedMessage);
});
it('should match tpa authentication failed error message', () => {
const expectedMessageSubstring = 'We are sorry, you are not authorized to access';
props = {
context: {
provider: 'Google',
},
errorCode: TPA_AUTHENTICATION_FAILURE,
failureCount: 0,
};
const registrationPage = mount(reduxWrapper(<IntlRegistrationFailure {...props} />));
expect(registrationPage.find('div.alert-heading').length).toEqual(1);
expect(registrationPage.find('div.alert').first().text()).toContain(expectedMessageSubstring);
});
// ******** test form buttons and fields ********
it('should match default button state', () => {
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />)));
expect(registrationPage.find('button[type="submit"] span').first().text()).toEqual('Create an account for free');
});
it('should match pending button state', () => {
store = mockStore({
...initialState,
register: {
...initialState.register,
submitState: PENDING_STATE,
},
});
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />)));
const button = registrationPage.find('button[type="submit"] span').first();
expect(button.find('.sr-only').text()).toEqual('pending');
});
it('should display opt-in/opt-out checkbox', () => {
mergeConfig({
MARKETING_EMAILS_OPT_IN: 'true',
});
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />)));
expect(registrationPage.find('div.form-field--checkbox').length).toEqual(1);
mergeConfig({
MARKETING_EMAILS_OPT_IN: '',
});
});
it('should show button label based on cta query params value', () => {
const buttonLabel = 'Register';
delete window.location;
window.location = { href: getConfig().BASE_URL, search: `?cta=${buttonLabel}` };
const registrationPage = mount(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />));
expect(registrationPage.find('button[type="submit"] span').first().text()).toEqual(buttonLabel);
});
it('should check registration conversion cookie', () => {
store = mockStore({
...initialState,
register: {
...initialState.register,
registrationResult: {
success: true,
},
},
});
renderer.create(routerWrapper(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />)));
expect(document.cookie).toMatch(`${getConfig().REGISTER_CONVERSION_COOKIE_NAME}=true`);
});
// ******** miscellaneous tests ********
it('should send page event when register page is rendered', () => {
mount(routerWrapper(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />)));
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register');
});
it('should send track event when user has successfully registered', () => {
store = mockStore({
...initialState,
register: {
...initialState.register,
registrationResult: {
success: true,
redirectUrl: 'https://test.com/testing-dashboard/',
},
},
});
delete window.location;
window.location = { href: getConfig().BASE_URL };
renderer.create(routerWrapper(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />)));
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', {});
});
it('should display error message based on the error code returned by API', () => {
store = mockStore({
...initialState,
register: {
...initialState.register,
registrationError: {
errorCode: INTERNAL_SERVER_ERROR,
},
},
});
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />))).find('EmbeddableRegistrationPage');
expect(registrationPage.find('div#validation-errors').first().text()).toContain(
'An error has occurred. Try refreshing the page, or check your internet connection.',
);
});
it('should call the postMessage API on registration success for embedded experience', () => {
mergeConfig({
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: true,
});
window.parent.postMessage = jest.fn();
delete window.location;
window.location = {
href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING),
search: '?host=http://localhost/host-website',
};
store = mockStore({
...initialState,
register: {
...initialState.register,
registrationResult: {
success: true,
},
},
commonComponents: {
...initialState.commonComponents,
optionalFields: {
extended_profile: [],
fields: {
level_of_education: { name: 'level_of_education', error_message: false },
},
},
},
});
const progressiveProfilingPage = mount(reduxWrapper(
<IntlEmbeddableRegistrationPage {...props} />,
));
progressiveProfilingPage.update();
expect(window.parent.postMessage).toHaveBeenCalledTimes(1);
});
it('should not display validations error on blur event when embedded variant is rendered', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: '?host=http://localhost/host-website' };
const registrationPage = mount(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />));
registrationPage.find('input#username').simulate('blur', { target: { value: '', name: 'username' } });
expect(registrationPage.find('div[feedback-for="username"]').exists()).toBeFalsy();
registrationPage.find('input[name="country"]').simulate('blur', { target: { value: '', name: 'country' } });
expect(registrationPage.find('div[feedback-for="country"]').exists()).toBeFalsy();
});
it('should set errors in temporary state when validations are returned by registration api', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: '?host=http://localhost/host-website' };
const usernameError = 'It looks like this username is already taken';
const emailError = 'This email is already associated with an existing or previous account';
store = mockStore({
...initialState,
register: {
...initialState.register,
registrationError: {
username: [{ userMessage: usernameError }],
email: [{ userMessage: emailError }],
},
},
});
const registrationPage = mount(routerWrapper(reduxWrapper(
<IntlEmbeddableRegistrationPage {...props} />),
)).find('EmbeddableRegistrationPage');
expect(registrationPage.find('div[feedback-for="username"]').exists()).toBeFalsy();
expect(registrationPage.find('div[feedback-for="email"]').exists()).toBeFalsy();
});
it('should clear error on focus for embedded experience also', () => {
delete window.location;
window.location = {
href: getConfig().BASE_URL.concat(REGISTER_PAGE),
search: '?host=http://localhost/host-website',
};
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />)));
registrationPage.find('button.btn-brand').simulate('click');
expect(registrationPage.find('div[feedback-for="password"]').text()).toContain(emptyFieldValidation.password);
registrationPage.find('input#password').simulate('focus');
expect(registrationPage.find('div[feedback-for="password"]').exists()).toBeFalsy();
});
});
describe('Test Configurable Fields', () => {
mergeConfig({
ENABLE_DYNAMIC_REGISTRATION_FIELDS: true,
SHOW_CONFIGURABLE_EDX_FIELDS: true,
});
it('should render fields returned by backend', () => {
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
fieldDescriptions: {
profession: { name: 'profession', type: 'text', label: 'Profession' },
terms_of_service: {
name: FIELDS.TERMS_OF_SERVICE,
error_message: 'You must agree to the Terms and Service agreement of our site',
},
},
},
});
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />)));
expect(registrationPage.find('#profession').exists()).toBeTruthy();
expect(registrationPage.find('#tos').exists()).toBeTruthy();
});
it('should submit form with fields returned by backend in payload', () => {
getLocale.mockImplementation(() => ('en-us'));
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
fieldDescriptions: {
profession: { name: 'profession', type: 'text', label: 'Profession' },
},
extendedProfile: ['profession'],
},
});
const payload = {
name: 'John Doe',
username: 'john_doe',
email: 'john.doe@example.com',
password: 'password1',
country: 'Pakistan',
honor_code: true,
profession: 'Engineer',
totalRegistrationTime: 0,
};
store.dispatch = jest.fn(store.dispatch);
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />)));
populateRequiredFields(registrationPage, payload);
registrationPage.find('input#profession').simulate('change', { target: { value: 'Engineer', name: 'profession' } });
registrationPage.find('button.btn-brand').simulate('click');
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
});
it('should show error messages for required fields on empty form submission', () => {
const professionError = 'Enter your profession';
const countryError = 'Select your country or region of residence';
const confirmEmailError = 'Enter your email';
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
fieldDescriptions: {
profession: {
name: 'profession', type: 'text', label: 'Profession', error_message: professionError,
},
confirm_email: {
name: 'confirm_email', type: 'text', label: 'Confirm Email', error_message: confirmEmailError,
},
country: { name: 'country' },
},
},
});
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />)));
registrationPage.find('button.btn-brand').simulate('click');
expect(registrationPage.find('#profession-error').last().text()).toEqual(professionError);
expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(countryError);
expect(registrationPage.find('#confirm_email-error').last().text()).toEqual(confirmEmailError);
});
it('should show error if email and confirm email fields do not match', () => {
store = mockStore({
...initialState,
register: {
...initialState.register,
registrationFormData: {
...initialState.register.registrationFormData,
configurableFormFields: {
...initialState.register.registrationFormData.configurableFormFields,
confirm_email: 'test2@yopmail.com',
},
formFields: {
...initialState.register.registrationFormData.formFields,
email: 'test1@yopmail.com',
},
},
},
commonComponents: {
...initialState.commonComponents,
fieldDescriptions: {
confirm_email: {
name: 'confirm_email', type: 'text', label: 'Confirm Email',
},
},
},
});
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />)));
registrationPage.find('input#confirm_email').simulate('blur');
registrationPage.find('button.btn-brand').simulate('click');
expect(registrationPage.find('div#confirm_email-error').text()).toEqual('The email addresses do not match.');
});
it('should run validations for configurable focused field on form submission', () => {
const professionError = 'Enter your profession';
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
fieldDescriptions: {
profession: {
name: 'profession', type: 'text', label: 'Profession', error_message: professionError,
},
},
},
});
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlEmbeddableRegistrationPage {...props} />)));
registrationPage.find('input#profession').simulate('focus', { target: { value: '', name: 'profession' } });
registrationPage.find('button.btn-brand').simulate('click');
expect(registrationPage.find('#profession-error').last().text()).toEqual(professionError);
});
});
});

View File

@@ -1,4 +1,4 @@
import { snakeCaseObject } from '@edx/frontend-platform';
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
import { LETTER_REGEX, NUMBER_REGEX } from '../../data/constants';
import messages from '../messages';
@@ -44,11 +44,12 @@ export const isFormValid = (
}
});
if (!configurableFormFields?.country?.displayValue) {
fieldErrors.country = formatMessage(messages['empty.country.field.error']);
isValid = false;
if (getConfig().SHOW_CONFIGURABLE_EDX_FIELDS) {
if (!configurableFormFields?.country?.displayValue) {
fieldErrors.country = formatMessage(messages['empty.country.field.error']);
isValid = false;
}
}
Object.keys(fieldDescriptions).forEach(key => {
if (key === 'country' && !configurableFormFields.country.displayValue) {
fieldErrors[key] = formatMessage(messages['empty.country.field.error']);

View File

@@ -1,5 +1,4 @@
export { default as RegistrationPage } from './RegistrationPage';
export { default as EmbeddableRegistrationPage } from './components/EmbeddableRegistrationPage';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';
export { storeName } from './data/reducers';

View File

@@ -38,6 +38,11 @@
max-width: 320px;
}
.main-heading {
position: relative;
z-index: 2;
}
.complete-your-profile {
font-weight: 700;
line-height: 1;