From d72fee2a7d2928e82924314f8bcb1bfdf3363e97 Mon Sep 17 00:00:00 2001 From: spaenleh Date: Thu, 21 Nov 2024 11:09:36 +0100 Subject: [PATCH 1/5] feat: add react-hook-form on register --- cypress/e2e/auth/register.cy.ts | 6 +- cypress/support/commands.ts | 4 +- public/locales/ar/auth.json | 5 - public/locales/de/auth.json | 8 +- public/locales/en/auth.json | 10 +- public/locales/es/auth.json | 5 - public/locales/fr/auth.json | 9 +- src/config/notifier.ts | 3 - src/config/selectors.ts | 4 +- .../components/register/AgreementForm.tsx | 83 ----- .../auth/components/register/EmailInput.tsx | 3 - .../register/EnableAnalyticsForm.tsx | 48 --- .../auth/components/register/Register.tsx | 225 -------------- .../auth/components/register/RegisterForm.tsx | 293 ++++++++++++++++++ .../components/signIn/PasswordLoginForm.tsx | 2 +- src/modules/auth/langs.ts | 7 +- src/modules/landing/header/RightHeader.tsx | 11 +- src/routes/account.tsx | 4 +- src/routes/auth.tsx | 1 - src/routes/auth/register.tsx | 64 +++- src/routes/auth/success.tsx | 1 + 21 files changed, 373 insertions(+), 423 deletions(-) delete mode 100644 src/modules/auth/components/register/AgreementForm.tsx delete mode 100644 src/modules/auth/components/register/EnableAnalyticsForm.tsx delete mode 100644 src/modules/auth/components/register/Register.tsx create mode 100644 src/modules/auth/components/register/RegisterForm.tsx diff --git a/cypress/e2e/auth/register.cy.ts b/cypress/e2e/auth/register.cy.ts index e4512cab6..a78e693b2 100644 --- a/cypress/e2e/auth/register.cy.ts +++ b/cypress/e2e/auth/register.cy.ts @@ -7,7 +7,7 @@ import { EMAIL_SIGN_UP_FIELD_ID, NAME_SIGN_UP_FIELD_ID, REGISTER_BUTTON_ID, - SIGN_UP_SAVE_ACTIONS_ID, + REGISTER_SAVE_ACTIONS_ID, SUCCESS_CONTENT_ID, } from '../../../src/config/selectors'; import { AUTH_MEMBERS } from '../../fixtures/members'; @@ -135,7 +135,7 @@ describe('Register', () => { }); it('Analytics should be visible and checked', () => { - cy.get(`#${SIGN_UP_SAVE_ACTIONS_ID}`) + cy.get(`#${REGISTER_SAVE_ACTIONS_ID}`) .should('exist') .should('be.checked'); }); @@ -148,7 +148,7 @@ describe('Register', () => { }); it('Register with analytics disabled', () => { - cy.get(`#${SIGN_UP_SAVE_ACTIONS_ID}`).click().should('not.be.checked'); + cy.get(`#${REGISTER_SAVE_ACTIONS_ID}`).click().should('not.be.checked'); cy.signUpAndCheck(AUTH_MEMBERS.GRAASP, true); cy.wait('@waitOnRegister') .its('request.body.enableSaveActions') diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 060e49f35..5c9097fd2 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -11,7 +11,7 @@ import { MAGIC_LINK_EMAIL_FIELD_ID, NAME_SIGN_UP_FIELD_ID, PASSWORD_SIGN_IN_FIELD_ID, - SIGN_UP_AGREEMENTS_CHECKBOX_ID, + REGISTER_AGREEMENTS_CHECKBOX_ID, } from '../../src/config/selectors'; import { fillPasswordSignInLayout, @@ -177,7 +177,7 @@ Cypress.Commands.add('checkErrorTextField', (id, flag) => { }); Cypress.Commands.add('agreeWithAllTerms', () => { - cy.get(`[data-cy="${SIGN_UP_AGREEMENTS_CHECKBOX_ID}"] input`) + cy.get(`[data-cy="${REGISTER_AGREEMENTS_CHECKBOX_ID}"] input`) .check() .should('be.checked'); }); diff --git a/public/locales/ar/auth.json b/public/locales/ar/auth.json index 01848df19..7f542ab12 100644 --- a/public/locales/ar/auth.json +++ b/public/locales/ar/auth.json @@ -26,11 +26,6 @@ "API_UNAVAILABLE_EXPLANATION": "يبدو أن خادم Graasp غير قابل للوصول في الوقت الحالي.", "API_UNAVAILABLE_INSTRUCTIONS": "الرجاء معاودة المحاولة في وقت لاحق.", "API_UNAVAILABLE_BUTTON": "أعد المحاولة", - "USER_AGREEMENTS_TERMS_OF_SERVICE": "شروط الخدمة", - "USER_AGREEMENTS_PRIVACY_POLICY": "سياسة الخصوصية", - "USER_AGREEMENTS_CHECKBOX_LABEL": "بالنقر على <0>{{sign_up_btn}}، فإنني أوافق على <1>{{terms_of_service}} و<2>{{privacy_policy}}.", - "TERMS_OF_SERVICE_LINK": "/terms", - "PRIVACY_POLICY_LINK": "/privacy", "INVITATIONS_LOADING_MESSAGE": "نحن في انتظار دعوتك، يرجى الاستعداد…", "USERNAME_TOO_SHORT_ERROR": "الرجاء إدخال اسم مستخدم يحتوي على أكثر من {{min}} حرفًا", "USERNAME_TOO_LONG_ERROR": "الرجاء إدخال اسم مستخدم تحت {{max}} من الأحرف", diff --git a/public/locales/de/auth.json b/public/locales/de/auth.json index 8aa796f68..f505b751e 100644 --- a/public/locales/de/auth.json +++ b/public/locales/de/auth.json @@ -16,7 +16,7 @@ "SIGN_IN_BUTTON": "Einloggen", "SIGN_IN_PASSWORD_BUTTON": "Einloggen", "LOGIN_TITLE": "Einloggen", - "SIGN_IN_LINK_TEXT": "Sie haben bereits ein Konto? Klicken Sie hier, um sich anzumelden", + "SIGN_IN_LINK_TEXT": "Sie haben bereits ein Konto? Einloggen", "SIGN_IN_SUCCESS_EMAIL_PROBLEM": "Wenn Sie keine E-Mail erhalten haben, überprüfen Sie Ihren Spam-Ordner. Wenn Sie eine institutionelle E-Mail (z. B. eine Schule oder ein Unternehmen) verwendet haben, wurde diese möglicherweise blockiert und Sie müssen warten, bis die E-Mail von Ihrem Spam-System freigegeben wird.", "SIGN_IN_SUCCESS_TEXT": "Bitte überprüfen Sie Ihren Emails {{email}}, um Ihren persönlichen Login-Link für den Zugriff auf Graasp abzurufen. Dies kann mehrere Minuten dauern.", "SIGN_IN_SUCCESS_TITLE": "Willkommen zurück!", @@ -31,11 +31,7 @@ "API_UNAVAILABLE_EXPLANATION": "Der Graasp-Server scheint momentan nicht erreichbar.", "API_UNAVAILABLE_INSTRUCTIONS": "Bitte versuchen Sie es später erneut.", "API_UNAVAILABLE_BUTTON": "Wiederholen", - "USER_AGREEMENTS_TERMS_OF_SERVICE": "Nutzungsbedingungen", - "USER_AGREEMENTS_PRIVACY_POLICY": "Datenschutzrichtlinie", - "USER_AGREEMENTS_CHECKBOX_LABEL": "Indem ich auf <0>{{sign_up_btn}} klicke, stimme ich den <1>{{terms_of_service}} und der <2>{{privacy_policy}} zu.", - "TERMS_OF_SERVICE_LINK": "/de/terms", - "PRIVACY_POLICY_LINK": "/de/privacy", + "USER_AGREEMENTS_CHECKBOX_LABEL": "Ich stimme den Nutzungsbedingungen und der Datenschutzrichtlinie zu.", "INVITATIONS_LOADING_MESSAGE": "Wir warten auf Ihre Einladung, bitte warten …", "USERNAME_TOO_SHORT_ERROR": "Bitte geben Sie einen Benutzernamen mit mehr als {{min}} Zeichen ein", "USERNAME_TOO_LONG_ERROR": "Bitte geben Sie einen Benutzernamen mit weniger als {{max}} Zeichen ein.", diff --git a/public/locales/en/auth.json b/public/locales/en/auth.json index ad57837c7..018c8b086 100644 --- a/public/locales/en/auth.json +++ b/public/locales/en/auth.json @@ -16,8 +16,7 @@ "SIGN_IN_BUTTON": "Receive a link to log in", "SIGN_IN_PASSWORD_BUTTON": "Log in", "LOGIN_TITLE": "Log In", - "SIGN_IN_LINK_TEXT": "Already have an account?", - "SIGN_IN_LINK_TEXT_BUTTON": "Log in", + "SIGN_IN_LINK_TEXT": "Already have an account? Log in", "SIGN_IN_SUCCESS_EMAIL_PROBLEM": "If you did not receive any email, check your spam. If you used an institutional email (ie. school, company), it might have been blocked and will have to wait until the email is released by your spam system.", "SIGN_IN_SUCCESS_TEXT": "Please check your inbox {{email}} to retrieve your personal login link to access Graasp. This can take several minutes.", "SIGN_IN_SUCCESS_TITLE": "Welcome back!", @@ -32,11 +31,8 @@ "API_UNAVAILABLE_EXPLANATION": "The Graasp server seems unreachable for the moment.", "API_UNAVAILABLE_INSTRUCTIONS": "Please try again later.", "API_UNAVAILABLE_BUTTON": "Retry", - "USER_AGREEMENTS_TERMS_OF_SERVICE": "terms of service", - "USER_AGREEMENTS_PRIVACY_POLICY": "privacy policy", - "USER_AGREEMENTS_CHECKBOX_LABEL": "I agree to the <1>{{terms_of_service}} and the <2>{{privacy_policy}}.", - "TERMS_OF_SERVICE_LINK": "/terms", - "PRIVACY_POLICY_LINK": "/privacy", + "USER_AGREEMENTS_CHECKBOX_LABEL": "I agree to the terms of service and the privacy policy.", + "USER_AGREEMENTS_REQUIRED": "You need to accept the terms of service and the privacy policy to create an account", "INVITATIONS_LOADING_MESSAGE": "We are looking for your invitation, please stand by…", "USERNAME_TOO_SHORT_ERROR": "Please enter a username with more than {{min}} characters", "USERNAME_TOO_LONG_ERROR": "Please enter a username under {{max}} characters", diff --git a/public/locales/es/auth.json b/public/locales/es/auth.json index 9d932725a..7d796a506 100644 --- a/public/locales/es/auth.json +++ b/public/locales/es/auth.json @@ -27,11 +27,6 @@ "API_UNAVAILABLE_EXPLANATION": "El servidor Graasp parece inaccesible por el momento.", "API_UNAVAILABLE_INSTRUCTIONS": "Por favor, inténtelo de nuevo más tarde.", "API_UNAVAILABLE_BUTTON": "Rever", - "USER_AGREEMENTS_TERMS_OF_SERVICE": "términos de servicio", - "USER_AGREEMENTS_PRIVACY_POLICY": "política de privacidad", - "USER_AGREEMENTS_CHECKBOX_LABEL": "Al hacer clic en <0>{{sign_up_btn}}, acepto los <1>{{terms_of_service}} y los <2>{{privacy_policy}}.", - "TERMS_OF_SERVICE_LINK": "/es/terms", - "PRIVACY_POLICY_LINK": "/es/privacy", "INVITATIONS_LOADING_MESSAGE": "Estamos esperando su invitación, por favor esperen...", "USERNAME_TOO_SHORT_ERROR": "Por favor ingrese un nombre de usuario con más de {{min}} caracteres", "USERNAME_TOO_LONG_ERROR": "Por favor ingrese un nombre de usuario con menos de {{max}} caracteres", diff --git a/public/locales/fr/auth.json b/public/locales/fr/auth.json index c46fa83ae..504d890aa 100644 --- a/public/locales/fr/auth.json +++ b/public/locales/fr/auth.json @@ -16,8 +16,7 @@ "SIGN_IN_BUTTON": "Se connecter avec un lien", "SIGN_IN_PASSWORD_BUTTON": "Se connecter", "LOGIN_TITLE": "Connection", - "SIGN_IN_LINK_TEXT": "Vous avez déjà un compte ?", - "SIGN_IN_LINK_TEXT_BUTTON": "Se connecter", + "SIGN_IN_LINK_TEXT": "Vous avez déjà un compte ? Se connecter", "SIGN_IN_SUCCESS_EMAIL_PROBLEM": "Si vous n'avez reçu aucun email, vérifiez vos spams. Si vous avez utilisé un email institutionnel (ex : école, entreprise), il est possible qu'il ait été bloqué et vous devrez attendre que l'email soit libéré par votre système de spam.", "SIGN_IN_SUCCESS_TEXT": "Veuillez consulter votre boîte de réception {{email}} pour récupérer votre lien de connexion personnel pour accéder à Graasp. Cela peut prendre plusieurs minutes.", "SIGN_IN_SUCCESS_TITLE": "Content de te revoir!", @@ -32,11 +31,7 @@ "API_UNAVAILABLE_EXPLANATION": "Le serveur Graasp semble inaccessible pour le moment.", "API_UNAVAILABLE_INSTRUCTIONS": "Veuillez réessayer plus tard.", "API_UNAVAILABLE_BUTTON": "Réessayer", - "USER_AGREEMENTS_TERMS_OF_SERVICE": "conditions d'utilisation", - "USER_AGREEMENTS_PRIVACY_POLICY": "politique de confidentialité", - "USER_AGREEMENTS_CHECKBOX_LABEL": "J'accepte les <1>{{terms_of_service}} et la <2>{{privacy_policy}}.", - "TERMS_OF_SERVICE_LINK": "/terms", - "PRIVACY_POLICY_LINK": "/privacy", + "USER_AGREEMENTS_CHECKBOX_LABEL": "J'accepte les conditions d'utilisation et la politique de confidentialité.", "INVITATIONS_LOADING_MESSAGE": "Nous cherchons votre invitation, merci de patienter…", "USERNAME_TOO_SHORT_ERROR": "Veuillez saisir un nom d'utilisateur de plus de {{min}} caractères", "USERNAME_TOO_LONG_ERROR": "Veuillez saisir un nom d'utilisateur de maximum {{max}} caractères", diff --git a/src/config/notifier.ts b/src/config/notifier.ts index a90f45ca4..f17b4c5bb 100644 --- a/src/config/notifier.ts +++ b/src/config/notifier.ts @@ -17,7 +17,6 @@ const { exportMemberDataRoutine, getInvitationRoutine, signInRoutine, - signUpRoutine, signInWithPasswordRoutine, } = routines; @@ -65,7 +64,6 @@ export default ({ // error messages // auth case signInRoutine.FAILURE: - case signUpRoutine.FAILURE: case signInWithPasswordRoutine.FAILURE: case getInvitationRoutine.FAILURE: case updatePasswordRoutine.FAILURE: @@ -80,7 +78,6 @@ export default ({ // success messages // auth case signInRoutine.SUCCESS: - case signUpRoutine.SUCCESS: case signInWithPasswordRoutine.SUCCESS: case updatePasswordRoutine.SUCCESS: case postPublicProfileRoutine.SUCCESS: diff --git a/src/config/selectors.ts b/src/config/selectors.ts index f9fa5496a..457068c17 100644 --- a/src/config/selectors.ts +++ b/src/config/selectors.ts @@ -121,12 +121,12 @@ export const RESET_PASSWORD_BACK_TO_LOGIN_BUTTON_ID = export const PASSWORD_SIGN_IN_FIELD_ID = 'passwordSignInField'; export const PASSWORD_SIGN_IN_BUTTON_ID = 'passwordSignInButton'; -export const SIGN_UP_AGREEMENTS_CHECKBOX_ID = 'signUpAgreementsCheckbox'; +export const REGISTER_AGREEMENTS_CHECKBOX_ID = 'registerAgreementsCheckbox'; export const SIGN_IN_BUTTON_ID = 'signInButton'; export const REGISTER_BUTTON_ID = 'registerButton'; export const REGISTER_HEADER_ID = 'registerHeader'; export const LOG_IN_HEADER_ID = 'logInHeader'; -export const SIGN_UP_SAVE_ACTIONS_ID = 'signUpSaveActions'; +export const REGISTER_SAVE_ACTIONS_ID = 'registerSaveActions'; export const EMAIL_SIGN_IN_METHOD_BUTTON_ID = 'emailSignInMethodButton'; export const USER_SWITCH_ID = 'userSwitch'; export const SUCCESS_CONTENT_ID = 'successContent'; diff --git a/src/modules/auth/components/register/AgreementForm.tsx b/src/modules/auth/components/register/AgreementForm.tsx deleted file mode 100644 index aa271558e..000000000 --- a/src/modules/auth/components/register/AgreementForm.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Trans, useTranslation } from 'react-i18next'; - -import { - Checkbox, - FormControlLabel, - FormGroup, - Typography, -} from '@mui/material'; - -import { Link } from '@tanstack/react-router'; - -import { NS } from '@/config/constants'; -import { SIGN_UP_AGREEMENTS_CHECKBOX_ID } from '@/config/selectors'; - -import { AUTH } from '~auth/langs'; - -import { UseAgreementForm } from '../../hooks/useAgreementForm'; - -type Props = { - useAgreementForm: UseAgreementForm; -}; - -export function AgreementForm({ useAgreementForm }: Props) { - const { t } = useTranslation(NS.Auth); - - const { userHasAcceptedAllTerms, updateUserAgreements, hasError } = - useAgreementForm; - - const errorColor = 'error'; - - return ( - - updateUserAgreements(checked)} - required - control={ - - } - label={ - - , - - _ - , - - _ - , - ]} - t={t} - /> - - } - /> - - ); -} diff --git a/src/modules/auth/components/register/EmailInput.tsx b/src/modules/auth/components/register/EmailInput.tsx index 7e0403780..509d4a4cd 100644 --- a/src/modules/auth/components/register/EmailInput.tsx +++ b/src/modules/auth/components/register/EmailInput.tsx @@ -15,7 +15,6 @@ type Props = { id?: string; disabled?: boolean; setValue: (str: string) => void; - onKeyPress?: React.KeyboardEventHandler; shouldValidate: boolean; autoFocus?: boolean; }; @@ -26,7 +25,6 @@ export function EmailInput({ id, disabled = false, setValue, - onKeyPress, shouldValidate = true, autoFocus = false, }: Props) { @@ -75,7 +73,6 @@ export function EmailInput({ id={id} type="email" disabled={disabled} - onKeyPress={onKeyPress} /> ); } diff --git a/src/modules/auth/components/register/EnableAnalyticsForm.tsx b/src/modules/auth/components/register/EnableAnalyticsForm.tsx deleted file mode 100644 index af40636fb..000000000 --- a/src/modules/auth/components/register/EnableAnalyticsForm.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useTranslation } from 'react-i18next'; - -import { - Checkbox, - FormControlLabel, - FormGroup, - Tooltip, - Typography, -} from '@mui/material'; - -import { NS } from '@/config/constants'; -import { SIGN_UP_SAVE_ACTIONS_ID } from '@/config/selectors'; - -import { AUTH } from '~auth/langs'; - -type Props = { - enableSaveActions: boolean; - onUpdateSaveActions: (enabled: boolean) => void; -}; - -export function EnableAnalyticsForm({ - enableSaveActions, - onUpdateSaveActions, -}: Props) { - const { t } = useTranslation(NS.Auth); - - return ( - - - onUpdateSaveActions(checked)} - /> - } - label={ - - {t(AUTH.SIGN_UP_SAVE_ACTIONS_LABEL)} - - } - /> - - - ); -} diff --git a/src/modules/auth/components/register/Register.tsx b/src/modules/auth/components/register/Register.tsx deleted file mode 100644 index 38e913c61..000000000 --- a/src/modules/auth/components/register/Register.tsx +++ /dev/null @@ -1,225 +0,0 @@ -import { ChangeEventHandler, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { LoadingButton } from '@mui/lab'; -import { FormControl, LinearProgress, Stack } from '@mui/material'; -import Typography from '@mui/material/Typography'; - -import { - MAX_USERNAME_LENGTH, - MIN_USERNAME_LENGTH, - RecaptchaAction, -} from '@graasp/sdk'; - -import { Link, useLocation, useNavigate } from '@tanstack/react-router'; - -import { NS } from '@/config/constants'; -import { hooks, mutations } from '@/config/queryClient'; -import { - EMAIL_SIGN_UP_FIELD_ID, - NAME_SIGN_UP_FIELD_ID, - REGISTER_BUTTON_ID, - REGISTER_HEADER_ID, -} from '@/config/selectors'; - -import { useRecaptcha } from '~auth/context/RecaptchaContext'; -import { useAgreementForm } from '~auth/hooks/useAgreementForm'; -import { useMobileAppLogin } from '~auth/hooks/useMobileAppLogin'; -import { AUTH } from '~auth/langs'; -import { emailValidator, nameValidator } from '~auth/validation'; - -import { ErrorDisplay } from '../common/ErrorDisplay'; -import { FormHeader } from '../common/FormHeader'; -import { StyledTextField } from '../common/StyledTextField'; -import { NameAdornment } from '../common/adornments'; -import { AgreementForm } from '../register/AgreementForm'; -import { EmailInput } from './EmailInput'; -import { EnableAnalyticsForm } from './EnableAnalyticsForm'; - -const { - SIGN_IN_LINK_TEXT, - SIGN_IN_LINK_TEXT_BUTTON, - NAME_FIELD_LABEL, - SIGN_UP_BUTTON, - INVITATIONS_LOADING_MESSAGE, -} = AUTH; - -type RegisterProps = { - search: { - url?: string; - invitationId?: string; - }; -}; - -export function Register({ search }: RegisterProps) { - const { t, i18n } = useTranslation(NS.Auth); - - const navigate = useNavigate(); - const location = useLocation(); - const { executeCaptcha } = useRecaptcha(); - - const { isMobile, challenge } = useMobileAppLogin(); - - const [email, setEmail] = useState(''); - const [name, setName] = useState(''); - const [nameError, setNameError] = useState(null); - // enable validation after first click - const [shouldValidate, setShouldValidate] = useState(false); - const [enableSaveActions, setEnableSaveActions] = useState(true); - - const agreementFormHook = useAgreementForm(); - const { verifyUserAgreements, userHasAcceptedAllTerms } = agreementFormHook; - - const { - mutateAsync: signUp, - isPending: isLoadingSignUp, - error: webRegisterError, - } = mutations.useSignUp(); - const { - mutateAsync: mobileSignUp, - isPending: isLoadingMobileSignUp, - error: mobileRegisterError, - } = mutations.useMobileSignUp(); - - const { - data: invitation, - isSuccess: isInvitationSuccess, - isLoading: isLoadingInvitations, - } = hooks.useInvitation(search.invitationId); - - useEffect(() => { - if (isInvitationSuccess && invitation) { - // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect - setEmail(invitation.email); - // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect - setName(invitation.name ?? ''); - } - }, [invitation, isInvitationSuccess]); - - // loading invitation - if (isLoadingInvitations) { - return ( - - {t(INVITATIONS_LOADING_MESSAGE)} - - - ); - } - - const registerError = webRegisterError || mobileRegisterError; - - const handleNameOnChange: ChangeEventHandler = (e) => { - const newName = e.target.value; - setName(newName); - if (shouldValidate) { - setNameError(nameValidator(newName)); - } - }; - - const handleRegister = async () => { - const lowercaseEmail = email.toLowerCase(); - const checkingEmail = emailValidator(lowercaseEmail); - const checkingUsername = nameValidator(name); - if (!verifyUserAgreements()) { - // should never happen - return; - } else if (checkingEmail || checkingUsername) { - setNameError(checkingUsername); - setShouldValidate(true); - } else { - const token = await executeCaptcha( - isMobile ? RecaptchaAction.SignUpMobile : RecaptchaAction.SignUp, - ); - await (isMobile - ? mobileSignUp({ - name: name.trim(), - email: lowercaseEmail, - captcha: token, - challenge, - lang: i18n.language, - enableSaveActions, - }) - : signUp({ - name: name.trim(), - email: lowercaseEmail, - captcha: token, - url: search.url, - lang: i18n.language, - enableSaveActions, - })); - - // navigate to success path - navigate({ - to: '/auth/success', - search: { email, back: location.pathname }, - }); - } - }; - - return ( - - - - - - - - setEnableSaveActions(enabled)} - /> - - - - - - {t(SIGN_UP_BUTTON)} - - - - {t(SIGN_IN_LINK_TEXT)} - - {t(SIGN_IN_LINK_TEXT_BUTTON)} - - - ); -} diff --git a/src/modules/auth/components/register/RegisterForm.tsx b/src/modules/auth/components/register/RegisterForm.tsx new file mode 100644 index 000000000..af3404ebd --- /dev/null +++ b/src/modules/auth/components/register/RegisterForm.tsx @@ -0,0 +1,293 @@ +import { Control, useController, useForm } from 'react-hook-form'; +import { Trans, useTranslation } from 'react-i18next'; + +import { LoadingButton } from '@mui/lab'; +import { + Checkbox, + FormControlLabel, + Stack, + Tooltip, + Typography, +} from '@mui/material'; + +import { + MAX_USERNAME_LENGTH, + MIN_USERNAME_LENGTH, + MemberConstants, + RecaptchaAction, + isEmail, +} from '@graasp/sdk'; + +import { Link, useLocation, useNavigate } from '@tanstack/react-router'; + +import { TypographyLink } from '@/components/ui/TypographyLink'; +import { NS } from '@/config/constants'; +import { mutations } from '@/config/queryClient'; +import { + EMAIL_SIGN_UP_FIELD_ID, + NAME_SIGN_UP_FIELD_ID, + REGISTER_AGREEMENTS_CHECKBOX_ID, + REGISTER_BUTTON_ID, + REGISTER_HEADER_ID, + REGISTER_SAVE_ACTIONS_ID, +} from '@/config/selectors'; + +import { useRecaptcha } from '~auth/context/RecaptchaContext'; +import { useMobileAppLogin } from '~auth/hooks/useMobileAppLogin'; +import { AUTH } from '~auth/langs'; + +import { ErrorDisplay } from '../common/ErrorDisplay'; +import { FormHeader } from '../common/FormHeader'; +import { StyledTextField } from '../common/StyledTextField'; +import { EmailAdornment, NameAdornment } from '../common/adornments'; + +const { SIGN_IN_LINK_TEXT, NAME_FIELD_LABEL, SIGN_UP_BUTTON } = AUTH; + +type RegisterInputs = { + name: string; + email: string; + enableSaveActions: boolean; + userHasAcceptedAllTerms: boolean; +}; + +function EnableAnalyticsForm({ + control, +}: { + control: Control; +}): JSX.Element { + const { field } = useController({ control, name: 'enableSaveActions' }); + const { t } = useTranslation(NS.Auth); + + return ( + + field.onChange(checked)} + /> + } + label={ + + {t(AUTH.SIGN_UP_SAVE_ACTIONS_LABEL)} + + } + /> + + ); +} + +export function AgreementForm({ + control, +}: { + control: Control; +}) { + const { t } = useTranslation(NS.Auth); + const { + field, + formState: { errors }, + } = useController({ + control, + name: 'userHasAcceptedAllTerms', + rules: { required: 'yes' }, + }); + const hasError = Boolean(errors.userHasAcceptedAllTerms?.message); + + return ( + <> + field.onChange(checked)} + color={hasError ? 'error' : 'primary'} + data-cy={REGISTER_AGREEMENTS_CHECKBOX_ID} + size="small" + /> + } + label={ + + , + privacy: , + }} + /> + + } + /> + {hasError && ( + + {t('USER_AGREEMENTS_REQUIRED')} + + )} + + ); +} + +type RegisterProps = { + search: { + url?: string; + invitationId?: string; + }; + initialData: { + name?: string; + email?: string; + }; +}; + +const defaultRedirection = new URL( + '/account', + window.location.origin, +).toString(); + +export function RegisterForm({ search, initialData }: RegisterProps) { + const { t, i18n } = useTranslation(NS.Auth); + + const navigate = useNavigate(); + const location = useLocation(); + const { executeCaptcha } = useRecaptcha(); + + const { isMobile, challenge } = useMobileAppLogin(); + + const { + register, + handleSubmit, + control, + formState: { errors }, + } = useForm({ + defaultValues: { enableSaveActions: true, ...initialData }, + }); + + const { + mutateAsync: signUp, + isPending: isLoadingSignUp, + error: webRegisterError, + } = mutations.useSignUp(); + const { + mutateAsync: mobileSignUp, + isPending: isLoadingMobileSignUp, + error: mobileRegisterError, + } = mutations.useMobileSignUp(); + + const registerError = webRegisterError || mobileRegisterError; + + const handleRegister = async (inputs: RegisterInputs) => { + // lowercase email + const email = inputs.email.toLowerCase(); + // trim username to remove extra whitespaces before and after + const name = inputs.name.trim(); + const token = await executeCaptcha( + isMobile ? RecaptchaAction.SignUpMobile : RecaptchaAction.SignUp, + ); + await (isMobile + ? mobileSignUp({ + name, + email, + captcha: token, + challenge, + lang: i18n.language, + enableSaveActions: inputs.enableSaveActions, + }) + : signUp({ + name, + email, + captcha: token, + url: search.url ?? defaultRedirection, + lang: i18n.language, + enableSaveActions: inputs.enableSaveActions, + })); + + // navigate to success path + navigate({ + to: '/auth/success', + search: { email, back: location.pathname }, + }); + }; + + const nameError = errors.name?.message satisfies string | undefined; + const emailError = errors.email?.message satisfies string | undefined; + + return ( + + + + + MemberConstants.USERNAME_FORBIDDEN_CHARS_REGEX.test( + name.trim(), + ) == false || t(AUTH.USERNAME_SPECIAL_CHARACTERS_ERROR), + })} + placeholder={t(NAME_FIELD_LABEL)} + // todo: Should we not allow users to change their name when creating a count from an invitation ? + // disabled={Boolean(invitation?.name)} + /> + isEmail(email, {}) || t('INVALID_EMAIL_ERROR'), + })} + placeholder={t('EMAIL_INPUT_PLACEHOLDER_REQUIRED')} + // do not allow to modify the email if it was provided + disabled={Boolean(initialData.email)} + /> + + + + + + 0)} + > + {t(SIGN_UP_BUTTON)} + + + + {t(SIGN_IN_LINK_TEXT)} + + + ); +} diff --git a/src/modules/auth/components/signIn/PasswordLoginForm.tsx b/src/modules/auth/components/signIn/PasswordLoginForm.tsx index 1325db9bb..b6a551739 100644 --- a/src/modules/auth/components/signIn/PasswordLoginForm.tsx +++ b/src/modules/auth/components/signIn/PasswordLoginForm.tsx @@ -126,7 +126,7 @@ export function PasswordLoginForm({ search }: PasswordLoginProps) { variant="caption" sx={{ textDecoration: 'none', - '&:hover': { color: (theme) => theme.palette.primary.main }, + '&:hover': { color: 'palette.primary.main' }, }} to="/auth/forgot-password" > diff --git a/src/modules/auth/langs.ts b/src/modules/auth/langs.ts index 0d5f22966..7f62efbc9 100644 --- a/src/modules/auth/langs.ts +++ b/src/modules/auth/langs.ts @@ -15,12 +15,13 @@ export const AUTH = { RESEND_EMAIL_BUTTON: 'RESEND_EMAIL_BUTTON', SIGN_IN_BUTTON: 'SIGN_IN_BUTTON', SIGN_IN_PASSWORD_BUTTON: 'SIGN_IN_PASSWORD_BUTTON', + LOGIN_TITLE: 'LOGIN_TITLE', SIGN_IN_LINK_TEXT: 'SIGN_IN_LINK_TEXT', - SIGN_IN_LINK_TEXT_BUTTON: 'SIGN_IN_LINK_TEXT_BUTTON', SIGN_IN_SUCCESS_EMAIL_PROBLEM: 'SIGN_IN_SUCCESS_EMAIL_PROBLEM', SIGN_IN_SUCCESS_TEXT: 'SIGN_IN_SUCCESS_TEXT', SIGN_IN_SUCCESS_TITLE: 'SIGN_IN_SUCCESS_TITLE', SIGN_UP_BUTTON: 'SIGN_UP_BUTTON', + REGISTER_TITLE: 'REGISTER_TITLE', SIGN_UP_LINK_TEXT: 'SIGN_UP_LINK_TEXT', SIGN_UP_SAVE_ACTIONS_LABEL: 'SIGN_UP_SAVE_ACTIONS_LABEL', SIGN_UP_SAVE_ACTIONS_TOOLTIP: 'SIGN_UP_SAVE_ACTIONS_TOOLTIP', @@ -30,11 +31,7 @@ export const AUTH = { API_UNAVAILABLE_EXPLANATION: 'API_UNAVAILABLE_EXPLANATION', API_UNAVAILABLE_INSTRUCTIONS: 'API_UNAVAILABLE_INSTRUCTIONS', API_UNAVAILABLE_BUTTON: 'API_UNAVAILABLE_BUTTON', - USER_AGREEMENTS_TERMS_OF_SERVICE: 'USER_AGREEMENTS_TERMS_OF_SERVICE', - USER_AGREEMENTS_PRIVACY_POLICY: 'USER_AGREEMENTS_PRIVACY_POLICY', USER_AGREEMENTS_CHECKBOX_LABEL: 'USER_AGREEMENTS_CHECKBOX_LABEL', - TERMS_OF_SERVICE_LINK: 'TERMS_OF_SERVICE_LINK', - PRIVACY_POLICY_LINK: 'PRIVACY_POLICY_LINK', INVITATIONS_LOADING_MESSAGE: 'INVITATIONS_LOADING_MESSAGE', USERNAME_TOO_SHORT_ERROR: 'USERNAME_TOO_SHORT_ERROR', USERNAME_SPECIAL_CHARACTERS_ERROR: 'USERNAME_SPECIAL_CHARACTERS_ERROR', diff --git a/src/modules/landing/header/RightHeader.tsx b/src/modules/landing/header/RightHeader.tsx index 4ea187442..a1ee0fb13 100644 --- a/src/modules/landing/header/RightHeader.tsx +++ b/src/modules/landing/header/RightHeader.tsx @@ -33,15 +33,8 @@ export function RightHeader(): JSX.Element { return ( - - {t('LOG_IN.BUTTON_TEXT')} - - - {t('REGISTER.BUTTON_TEXT')} - + {t('LOG_IN.BUTTON_TEXT')} + {t('REGISTER.BUTTON_TEXT')} ); } diff --git a/src/routes/account.tsx b/src/routes/account.tsx index 633c2690b..d2495b466 100644 --- a/src/routes/account.tsx +++ b/src/routes/account.tsx @@ -7,14 +7,14 @@ import { LOG_IN_PAGE_PATH } from '@/config/paths'; import { PageWrapper } from '~account/PageWrapper'; export const Route = createFileRoute('/account')({ - beforeLoad: ({ context, location }) => { + beforeLoad: ({ context }) => { // check if the user is authenticated. // if not, redirect to `/auth/login` so the user can log in their account if (!context.auth.isAuthenticated) { throw redirect({ to: LOG_IN_PAGE_PATH, search: { - url: `${window.location.origin}${location.href}`, + url: window.location.href, }, }); } diff --git a/src/routes/auth.tsx b/src/routes/auth.tsx index 5a8379494..6c54a85b4 100644 --- a/src/routes/auth.tsx +++ b/src/routes/auth.tsx @@ -16,7 +16,6 @@ export const Route = createFileRoute('/auth')({ }); } }, - component: RouteComponent, }); diff --git a/src/routes/auth/register.tsx b/src/routes/auth/register.tsx index 35ac2aba2..76c035531 100644 --- a/src/routes/auth/register.tsx +++ b/src/routes/auth/register.tsx @@ -1,27 +1,79 @@ -import { createFileRoute } from '@tanstack/react-router'; +import { useTranslation } from 'react-i18next'; + +import { Alert, LinearProgress, Stack, Typography } from '@mui/material'; + +import { createFileRoute, retainSearchParams } from '@tanstack/react-router'; import { zodSearchValidator } from '@tanstack/router-zod-adapter'; import { z } from 'zod'; +import { NS } from '@/config/constants'; +import { hooks } from '@/config/queryClient'; + import { LeftContentContainer } from '~auth/components/LeftContentContainer'; -import { Register } from '~auth/components/register/Register'; +import { RegisterForm } from '~auth/components/register/RegisterForm'; const registerSearchSchema = z.object({ + invitationId: z.string().uuid().optional(), url: z.string().url().optional(), m: z.string().optional(), - invitationId: z.string().uuid().optional(), }); export const Route = createFileRoute('/auth/register')({ validateSearch: zodSearchValidator(registerSearchSchema), - + search: { middlewares: [retainSearchParams(['url'])] }, component: RegisterPage, }); -function RegisterPage() { +function RegisterWithoutInvitation(): JSX.Element { const search = Route.useSearch(); return ( - + ); } + +function RegisterWithInvitation() { + const search = Route.useSearch(); + const { t } = useTranslation(NS.Auth); + + const { + data: invitation, + isPending: isLoadingInvitations, + error, + } = hooks.useInvitation(search.invitationId); + + if (invitation) { + return ( + + + + ); + } + + // invitations loading + if (isLoadingInvitations) { + return ( + + + {t('INVITATIONS_LOADING_MESSAGE')} + + + + ); + } + + return {error.message}; +} + +function RegisterPage() { + const { invitationId } = Route.useSearch(); + if (invitationId) { + return ; + } + + return ; +} diff --git a/src/routes/auth/success.tsx b/src/routes/auth/success.tsx index 1b4c23c63..5f2b4da7c 100644 --- a/src/routes/auth/success.tsx +++ b/src/routes/auth/success.tsx @@ -73,6 +73,7 @@ function RouteComponent() { display="flex" justifyContent="center" alignItems="center" + gap={2} > {t('SIGN_IN_SUCCESS_TITLE')} From fedbef6bae2cb45c3daf83ca0eb6b9fb20a393f2 Mon Sep 17 00:00:00 2001 From: spaenleh Date: Thu, 21 Nov 2024 14:24:17 +0100 Subject: [PATCH 2/5] chore: do not rename the file --- .../auth/components/register/{RegisterForm.tsx => Register.tsx} | 0 src/routes/auth/register.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/modules/auth/components/register/{RegisterForm.tsx => Register.tsx} (100%) diff --git a/src/modules/auth/components/register/RegisterForm.tsx b/src/modules/auth/components/register/Register.tsx similarity index 100% rename from src/modules/auth/components/register/RegisterForm.tsx rename to src/modules/auth/components/register/Register.tsx diff --git a/src/routes/auth/register.tsx b/src/routes/auth/register.tsx index 76c035531..a54e66fbe 100644 --- a/src/routes/auth/register.tsx +++ b/src/routes/auth/register.tsx @@ -10,7 +10,7 @@ import { NS } from '@/config/constants'; import { hooks } from '@/config/queryClient'; import { LeftContentContainer } from '~auth/components/LeftContentContainer'; -import { RegisterForm } from '~auth/components/register/RegisterForm'; +import { RegisterForm } from '~auth/components/register/Register'; const registerSearchSchema = z.object({ invitationId: z.string().uuid().optional(), From 7a9bcd87285916c7934f3ff6d744f431cdee9bcf Mon Sep 17 00:00:00 2001 From: spaenleh Date: Thu, 21 Nov 2024 16:00:39 +0100 Subject: [PATCH 3/5] fix: most of the tests --- cypress/e2e/auth/register.cy.ts | 245 +++++++++--------- cypress/e2e/auth/util.ts | 6 +- cypress/support/commands.ts | 4 +- public/locales/en/account.json | 2 + public/locales/en/auth.json | 1 - public/locales/en/common.json | 7 + .../auth/components/register/Register.tsx | 31 ++- 7 files changed, 159 insertions(+), 137 deletions(-) diff --git a/cypress/e2e/auth/register.cy.ts b/cypress/e2e/auth/register.cy.ts index a78e693b2..924295b6b 100644 --- a/cypress/e2e/auth/register.cy.ts +++ b/cypress/e2e/auth/register.cy.ts @@ -13,146 +13,149 @@ import { import { AUTH_MEMBERS } from '../../fixtures/members'; import { checkInvitationFields, fillSignUpLayout } from './util'; -describe('Register', () => { - describe('Must accept all terms to register', () => { - beforeEach(() => { - cy.visit('/auth/register'); - cy.intercept({ method: 'post', pathname: '/register' }, ({ reply }) => { - return reply({ - statusCode: StatusCodes.NO_CONTENT, - }); +describe('Must accept all terms to register', () => { + beforeEach(() => { + cy.visit('/auth/register'); + cy.intercept({ method: 'post', pathname: '/register' }, ({ reply }) => { + return reply({ + statusCode: StatusCodes.NO_CONTENT, }); }); + }); - it('Cannot register without accepting terms', () => { - cy.get(`#${REGISTER_BUTTON_ID}`).should('be.disabled'); - }); + it('Cannot register without accepting terms', () => { + cy.get(`#${REGISTER_BUTTON_ID}`).click(); + cy.get(`#${REGISTER_BUTTON_ID}`).should('be.disabled'); + }); - it('Register is available when accepting all terms', () => { - fillSignUpLayout({ name: 'name', email: 'email' }); - cy.get(`#${REGISTER_BUTTON_ID}`).should('be.disabled'); + it('Register is available when accepting all terms', () => { + fillSignUpLayout({ name: 'name', email: 'email@example.com' }); + cy.get(`#${REGISTER_BUTTON_ID}`).click(); + cy.get(`#${REGISTER_BUTTON_ID}`).should('be.disabled'); - cy.agreeWithAllTerms(); - cy.get(`#${REGISTER_BUTTON_ID}`).should('not.be.disabled'); - }); + cy.agreeWithAllTerms(); + cy.get(`#${REGISTER_BUTTON_ID}`).should('not.be.disabled'); }); +}); - describe('Name and Email Validation', () => { - it('Register', () => { - const { GRAASP, WRONG_NAME, INVALID_EMAIL: WRONG_EMAIL } = AUTH_MEMBERS; - cy.visit('/auth/register'); - cy.intercept({ method: 'post', pathname: '/register' }, ({ reply }) => { - return reply({ - statusCode: StatusCodes.NO_CONTENT, - }); +describe('Name and Email Validation', () => { + beforeEach(() => { + cy.visit('/auth/register'); + cy.intercept({ method: 'post', pathname: '/register' }, ({ reply }) => { + return reply({ + statusCode: StatusCodes.NO_CONTENT, }); + }); + }); - // Signing up with a wrong name and right email - cy.signUpAndCheck(WRONG_NAME, true); - // Signing up with a wrong email and right name - cy.signUpAndCheck(WRONG_EMAIL, true); - // Signing up with right email and name - cy.signUpAndCheck(GRAASP, true); + it('Wrong name', () => { + // Signing up with a wrong name and right email + cy.signUpAndCheck(AUTH_MEMBERS.WRONG_NAME, true); + }); - cy.get(`#${SUCCESS_CONTENT_ID}`).should('be.visible'); - }); + it('Invalid Email', () => { + // Signing up with a wrong email and right name + cy.signUpAndCheck(AUTH_MEMBERS.INVALID_EMAIL, true); + }); - it('Register from invitation with name', () => { - const invitation = { - id: v4(), - name: 'name', - email: 'email', - }; - cy.intercept( - API_ROUTES.buildGetInvitationRoute(invitation.id), - ({ reply }) => { - reply(invitation); - }, - ); - const search = new URLSearchParams(); - search.set('invitationId', invitation.id); - cy.visit(`/auth/register?${search.toString()}`); - checkInvitationFields(invitation); - }); + it('Correct inputs', () => { + // Signing up with right email and name + cy.signUpAndCheck(AUTH_MEMBERS.GRAASP, true); + cy.get(`#${SUCCESS_CONTENT_ID}`).should('be.visible'); + }); - it('Register from invitation without name', () => { - const invitation = { - id: v4(), - email: 'email', - }; - cy.intercept( - API_ROUTES.buildGetInvitationRoute(invitation.id), - invitation, - ); - const search = new URLSearchParams(); - search.set('invitationId', invitation.id); - cy.visit(`/auth/register?${search.toString()}`); - checkInvitationFields(invitation); - }); + it('Username can not contain special characters', () => { + const badUsername = '<
%^\'"'; + + cy.visit('/auth/register'); + cy.get(`#${NAME_SIGN_UP_FIELD_ID}`).clear(); + cy.get(`#${NAME_SIGN_UP_FIELD_ID}`).type(badUsername); + cy.get(`#${EMAIL_SIGN_UP_FIELD_ID}`).clear(); + cy.get(`#${EMAIL_SIGN_UP_FIELD_ID}`).type('test@test.lol'); + cy.agreeWithAllTerms(); + cy.get(`#${REGISTER_BUTTON_ID}`).click(); + + // The helper text should display the message about special characters + cy.get('[id$=-helper-text]').should( + 'have.text', + 'User name must not contain quotes, anti-slash, <, >, ^, %', + ); + }); +}); - it('Register with invalid invitation', () => { - const invitation = { - id: v4(), - email: 'email', - }; - cy.intercept(API_ROUTES.buildGetInvitationRoute(invitation.id), { - statusCode: 404, - body: { message: '404 Not Found!' }, - }); - const search = new URLSearchParams(); - search.set('invitationId', invitation.id); - cy.visit(`/auth/register?${search.toString()}`); - cy.get(`#${REGISTER_BUTTON_ID}`).should('be.visible'); - }); +describe.only('Invitations', () => { + it('Register from invitation with name', () => { + const invitation = { + id: v4(), + name: 'name', + email: 'email', + }; + cy.intercept( + API_ROUTES.buildGetInvitationRoute(invitation.id), + ({ reply }) => { + reply(invitation); + }, + ); + const search = new URLSearchParams(); + search.set('invitationId', invitation.id); + cy.visit(`/auth/register?${search.toString()}`); + checkInvitationFields(invitation); + }); - it('Username can not contain special characters', () => { - const badUsername = '<
%^\'"'; - - cy.visit('/auth/register'); - cy.get(`#${NAME_SIGN_UP_FIELD_ID}`).clear(); - cy.get(`#${NAME_SIGN_UP_FIELD_ID}`).type(badUsername); - cy.get(`#${EMAIL_SIGN_UP_FIELD_ID}`).clear(); - cy.get(`#${EMAIL_SIGN_UP_FIELD_ID}`).type('test@test.lol'); - cy.agreeWithAllTerms(); - cy.get(`#${REGISTER_BUTTON_ID}`).click(); - - // The helper text should display the message about special characters - cy.get('[id$=-helper-text]').should( - 'have.text', - 'User name must not contain " ", ", <, >, ^, %, \\', - ); - }); + it('Register from invitation without name', () => { + const invitation = { + id: v4(), + email: 'email', + }; + cy.intercept(API_ROUTES.buildGetInvitationRoute(invitation.id), invitation); + const search = new URLSearchParams(); + search.set('invitationId', invitation.id); + cy.visit(`/auth/register?${search.toString()}`); + checkInvitationFields(invitation); }); - describe('Defining analytics on register', () => { - beforeEach(() => { - cy.visit('/auth/register'); - cy.intercept({ method: 'post', pathname: '/register' }, ({ reply }) => { - return reply({ - statusCode: StatusCodes.NO_CONTENT, - }); - }).as('waitOnRegister'); + it('Register with invalid invitation', () => { + const invitation = { + id: v4(), + email: 'email', + }; + cy.intercept(API_ROUTES.buildGetInvitationRoute(invitation.id), { + statusCode: 404, + body: { message: '404 Not Found!' }, }); + const search = new URLSearchParams(); + search.set('invitationId', invitation.id); + cy.visit(`/auth/register?${search.toString()}`); + cy.get(`#${REGISTER_BUTTON_ID}`).should('be.visible'); + }); +}); - it('Analytics should be visible and checked', () => { - cy.get(`#${REGISTER_SAVE_ACTIONS_ID}`) - .should('exist') - .should('be.checked'); - }); +describe.only('Defining analytics on register', () => { + beforeEach(() => { + cy.visit('/auth/register'); + cy.intercept({ method: 'post', pathname: '/register' }, ({ reply }) => { + return reply({ + statusCode: StatusCodes.NO_CONTENT, + }); + }).as('waitOnRegister'); + }); - it('Register with analytics enabled', () => { - cy.signUpAndCheck(AUTH_MEMBERS.GRAASP, true); - cy.wait('@waitOnRegister') - .its('request.body.enableSaveActions') - .should('eq', true); - }); + it('Analytics should be visible and checked', () => { + cy.get(`#${REGISTER_SAVE_ACTIONS_ID}`).should('exist').should('be.checked'); + }); - it('Register with analytics disabled', () => { - cy.get(`#${REGISTER_SAVE_ACTIONS_ID}`).click().should('not.be.checked'); - cy.signUpAndCheck(AUTH_MEMBERS.GRAASP, true); - cy.wait('@waitOnRegister') - .its('request.body.enableSaveActions') - .should('eq', false); - }); + it('Register with analytics enabled', () => { + cy.signUpAndCheck(AUTH_MEMBERS.GRAASP, true); + cy.wait('@waitOnRegister') + .its('request.body.enableSaveActions') + .should('eq', true); + }); + + it('Register with analytics disabled', () => { + cy.get(`#${REGISTER_SAVE_ACTIONS_ID}`).click().should('not.be.checked'); + cy.signUpAndCheck(AUTH_MEMBERS.GRAASP, true); + cy.wait('@waitOnRegister') + .its('request.body.enableSaveActions') + .should('eq', false); }); }); diff --git a/cypress/e2e/auth/util.ts b/cypress/e2e/auth/util.ts index 7d690a000..00dc39482 100644 --- a/cypress/e2e/auth/util.ts +++ b/cypress/e2e/auth/util.ts @@ -32,9 +32,7 @@ export const checkInvitationFields = ({ email: string; }): void => { if (name) { - cy.get(`#${NAME_SIGN_UP_FIELD_ID}`) - .should('have.value', name) - .should('be.disabled'); + cy.get(`#${NAME_SIGN_UP_FIELD_ID}`).should('have.value', name); } cy.get(`#${EMAIL_SIGN_UP_FIELD_ID}`) .should('have.value', email) @@ -51,7 +49,7 @@ export const submitSignIn = (): void => { cy.get(`#${SIGN_IN_BUTTON_ID}`).click(); }; -export const submitSignUp = (): void => { +export const submitRegister = (): void => { cy.get(`#${REGISTER_BUTTON_ID}`).click(); }; diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 5c9097fd2..2136327fa 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -18,8 +18,8 @@ import { fillSignInByMailLayout, fillSignUpLayout, submitPasswordSignIn, + submitRegister, submitSignIn, - submitSignUp, } from '../e2e/auth/util'; import { CURRENT_MEMBER, @@ -187,7 +187,7 @@ Cypress.Commands.add('signUpAndCheck', (user, acceptAllTerms) => { if (acceptAllTerms) { cy.agreeWithAllTerms(); } - submitSignUp(); + submitRegister(); cy.checkErrorTextField(NAME_SIGN_UP_FIELD_ID, user.nameValid); cy.checkErrorTextField(EMAIL_SIGN_UP_FIELD_ID, user.emailValid); diff --git a/public/locales/en/account.json b/public/locales/en/account.json index 5bebd26c8..c6c503fff 100644 --- a/public/locales/en/account.json +++ b/public/locales/en/account.json @@ -61,6 +61,8 @@ "PROFILE_DELETE_CONFIRMATION_VALUE": "delete", "USERNAME_EMPTY_ERROR": "The field cannot be empty", "USERNAME_LENGTH_ERROR": "Username must be between {{min}} and {{max}} characters long", + "USERNAME_MAX_LENGTH_ERROR": "Username must be less than {{max}} characters", + "USERNAME_MIN_LENGTH_ERROR": "Username must be longer than {{min}} characters", "USERNAME_SPECIAL_CHARACTERS_ERROR": "User name must not contain \" \", \", <, >, ^, %, \\", "EDIT_BUTTON_LABEL": "Edit", "CONFIGURE_BUTTON_LABEL": "Configure", diff --git a/public/locales/en/auth.json b/public/locales/en/auth.json index 018c8b086..9d099105b 100644 --- a/public/locales/en/auth.json +++ b/public/locales/en/auth.json @@ -36,7 +36,6 @@ "INVITATIONS_LOADING_MESSAGE": "We are looking for your invitation, please stand by…", "USERNAME_TOO_SHORT_ERROR": "Please enter a username with more than {{min}} characters", "USERNAME_TOO_LONG_ERROR": "Please enter a username under {{max}} characters", - "USERNAME_SPECIAL_CHARACTERS_ERROR": "User name must not contain \" \", \", <, >, ^, %, \\", "INVALID_EMAIL_ERROR": "This does not look like a valid email address", "EMPTY_EMAIL_ERROR": "An email address is required, this field can not be empty", "REQUIRED_FIELD_ERROR": "This field is required", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index d984e6365..e06abb78d 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -38,5 +38,12 @@ }, "ERRORS": { "UNEXPECTED": "An unexpected Error occurred" + }, + "FIELD_ERROR": { + "REQUIRED": "This field is required", + "USERNAME_MIN_LENGTH": "User name must be longer than {{min}} characters", + "USERNAME_MAX_LENGTH": "User name must be less than {{max}} characters", + "USERNAME_SPECIAL_CHARACTERS": "User name must not contain quotes, anti-slash, <, >, ^, %", + "INVALID_EMAIL": "This does not look like a valid email address" } } diff --git a/src/modules/auth/components/register/Register.tsx b/src/modules/auth/components/register/Register.tsx index af3404ebd..4a9e51968 100644 --- a/src/modules/auth/components/register/Register.tsx +++ b/src/modules/auth/components/register/Register.tsx @@ -151,6 +151,9 @@ const defaultRedirection = new URL( export function RegisterForm({ search, initialData }: RegisterProps) { const { t, i18n } = useTranslation(NS.Auth); + const { t: translateCommon } = useTranslation(NS.Common, { + keyPrefix: 'FIELD_ERROR', + }); const navigate = useNavigate(); const location = useLocation(); @@ -238,13 +241,22 @@ export function RegisterForm({ search, initialData }: RegisterProps) { helperText={nameError} error={Boolean(nameError)} {...register('name', { - required: t('REQUIRED_FIELD_ERROR'), - maxLength: MAX_USERNAME_LENGTH, - minLength: MIN_USERNAME_LENGTH, + required: translateCommon('REQUIRED'), + maxLength: { + value: MAX_USERNAME_LENGTH, + message: translateCommon('USERNAME_MAX_LENGTH', { + max: MAX_USERNAME_LENGTH, + }), + }, + minLength: { + value: MIN_USERNAME_LENGTH, + message: translateCommon('USERNAME_MIN_LENGTH', { + min: MIN_USERNAME_LENGTH, + }), + }, validate: (name) => - MemberConstants.USERNAME_FORBIDDEN_CHARS_REGEX.test( - name.trim(), - ) == false || t(AUTH.USERNAME_SPECIAL_CHARACTERS_ERROR), + MemberConstants.USERNAME_FORMAT_REGEX.test(name.trim()) || + translateCommon('USERNAME_SPECIAL_CHARACTERS'), })} placeholder={t(NAME_FIELD_LABEL)} // todo: Should we not allow users to change their name when creating a count from an invitation ? @@ -258,12 +270,13 @@ export function RegisterForm({ search, initialData }: RegisterProps) { }, }} variant="outlined" - type="email" + // type="email" error={Boolean(emailError)} helperText={emailError} {...register('email', { - required: t('REQUIRED_FIELD_ERROR'), - validate: (email) => isEmail(email, {}) || t('INVALID_EMAIL_ERROR'), + required: translateCommon('REQUIRED'), + validate: (email) => + isEmail(email, {}) || translateCommon('INVALID_EMAIL'), })} placeholder={t('EMAIL_INPUT_PLACEHOLDER_REQUIRED')} // do not allow to modify the email if it was provided From e148bfad995afa2fd24005370e1b82e2ca5844db Mon Sep 17 00:00:00 2001 From: spaenleh Date: Thu, 21 Nov 2024 16:11:43 +0100 Subject: [PATCH 4/5] fix: tests --- cypress/e2e/auth/register.cy.ts | 6 ++-- public/locales/en/auth.json | 3 +- src/routes/auth/register.tsx | 55 +++++++++++++++------------------ 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/cypress/e2e/auth/register.cy.ts b/cypress/e2e/auth/register.cy.ts index 924295b6b..c859f7016 100644 --- a/cypress/e2e/auth/register.cy.ts +++ b/cypress/e2e/auth/register.cy.ts @@ -83,7 +83,7 @@ describe('Name and Email Validation', () => { }); }); -describe.only('Invitations', () => { +describe('Invitations', () => { it('Register from invitation with name', () => { const invitation = { id: v4(), @@ -121,7 +121,7 @@ describe.only('Invitations', () => { }; cy.intercept(API_ROUTES.buildGetInvitationRoute(invitation.id), { statusCode: 404, - body: { message: '404 Not Found!' }, + body: { message: 'Invitation not found!' }, }); const search = new URLSearchParams(); search.set('invitationId', invitation.id); @@ -130,7 +130,7 @@ describe.only('Invitations', () => { }); }); -describe.only('Defining analytics on register', () => { +describe('Defining analytics on register', () => { beforeEach(() => { cy.visit('/auth/register'); cy.intercept({ method: 'post', pathname: '/register' }, ({ reply }) => { diff --git a/public/locales/en/auth.json b/public/locales/en/auth.json index 9d099105b..5f11bd0ba 100644 --- a/public/locales/en/auth.json +++ b/public/locales/en/auth.json @@ -33,7 +33,8 @@ "API_UNAVAILABLE_BUTTON": "Retry", "USER_AGREEMENTS_CHECKBOX_LABEL": "I agree to the terms of service and the privacy policy.", "USER_AGREEMENTS_REQUIRED": "You need to accept the terms of service and the privacy policy to create an account", - "INVITATIONS_LOADING_MESSAGE": "We are looking for your invitation, please stand by…", + "INVITATION_LOADING_MESSAGE": "We are looking for your invitation, please stand by…", + "INVITATION_NOT_FOUND_MESSAGE": "We could not find an invitation for your request. You can still create an account.", "USERNAME_TOO_SHORT_ERROR": "Please enter a username with more than {{min}} characters", "USERNAME_TOO_LONG_ERROR": "Please enter a username under {{max}} characters", "INVALID_EMAIL_ERROR": "This does not look like a valid email address", diff --git a/src/routes/auth/register.tsx b/src/routes/auth/register.tsx index a54e66fbe..93a4562c1 100644 --- a/src/routes/auth/register.tsx +++ b/src/routes/auth/register.tsx @@ -21,59 +21,54 @@ const registerSearchSchema = z.object({ export const Route = createFileRoute('/auth/register')({ validateSearch: zodSearchValidator(registerSearchSchema), search: { middlewares: [retainSearchParams(['url'])] }, - component: RegisterPage, -}); - -function RegisterWithoutInvitation(): JSX.Element { - const search = Route.useSearch(); - return ( + component: () => ( - + - ); -} + ), +}); function RegisterWithInvitation() { const search = Route.useSearch(); const { t } = useTranslation(NS.Auth); - const { - data: invitation, - isPending: isLoadingInvitations, - error, - } = hooks.useInvitation(search.invitationId); + const { data: invitation, isPending: isLoadingInvitations } = + hooks.useInvitation(search.invitationId); if (invitation) { return ( - - - + ); } // invitations loading if (isLoadingInvitations) { return ( - - - {t('INVITATIONS_LOADING_MESSAGE')} - - - + + {t('INVITATION_LOADING_MESSAGE')} + + ); } - return {error.message}; + return ( + + + {t('INVITATION_NOT_FOUND_MESSAGE')} + + + + ); } function RegisterPage() { - const { invitationId } = Route.useSearch(); - if (invitationId) { + const search = Route.useSearch(); + if (search.invitationId) { return ; } - return ; + return ; } From da869bd31f4537ada137707fd80ac763740eb1e5 Mon Sep 17 00:00:00 2001 From: spaenleh Date: Fri, 22 Nov 2024 13:47:46 +0100 Subject: [PATCH 5/5] fix: review comments --- .../auth/components/register/Register.tsx | 56 ++++++++----------- src/routes/auth/register.tsx | 4 +- 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/src/modules/auth/components/register/Register.tsx b/src/modules/auth/components/register/Register.tsx index 4a9e51968..ff11c09b6 100644 --- a/src/modules/auth/components/register/Register.tsx +++ b/src/modules/auth/components/register/Register.tsx @@ -41,8 +41,6 @@ import { FormHeader } from '../common/FormHeader'; import { StyledTextField } from '../common/StyledTextField'; import { EmailAdornment, NameAdornment } from '../common/adornments'; -const { SIGN_IN_LINK_TEXT, NAME_FIELD_LABEL, SIGN_UP_BUTTON } = AUTH; - type RegisterInputs = { name: string; email: string; @@ -50,11 +48,11 @@ type RegisterInputs = { userHasAcceptedAllTerms: boolean; }; -function EnableAnalyticsForm({ - control, -}: { +type FormElementProps = { control: Control; -}): JSX.Element { +}; + +function EnableAnalyticsForm({ control }: FormElementProps): JSX.Element { const { field } = useController({ control, name: 'enableSaveActions' }); const { t } = useTranslation(NS.Auth); @@ -79,11 +77,7 @@ function EnableAnalyticsForm({ ); } -export function AgreementForm({ - control, -}: { - control: Control; -}) { +export function AgreementForm({ control }: FormElementProps) { const { t } = useTranslation(NS.Auth); const { field, @@ -91,9 +85,9 @@ export function AgreementForm({ } = useController({ control, name: 'userHasAcceptedAllTerms', - rules: { required: 'yes' }, + rules: { required: t('USER_AGREEMENTS_REQUIRED') }, }); - const hasError = Boolean(errors.userHasAcceptedAllTerms?.message); + const validationError = errors.userHasAcceptedAllTerms?.message; return ( <> @@ -102,7 +96,7 @@ export function AgreementForm({ field.onChange(checked)} - color={hasError ? 'error' : 'primary'} + color={validationError ? 'error' : 'primary'} data-cy={REGISTER_AGREEMENTS_CHECKBOX_ID} size="small" /> @@ -111,7 +105,7 @@ export function AgreementForm({ } /> - {hasError && ( + {validationError && ( - {t('USER_AGREEMENTS_REQUIRED')} + {validationError} )} @@ -138,7 +132,7 @@ type RegisterProps = { url?: string; invitationId?: string; }; - initialData: { + initialData?: { name?: string; email?: string; }; @@ -158,9 +152,7 @@ export function RegisterForm({ search, initialData }: RegisterProps) { const navigate = useNavigate(); const location = useLocation(); const { executeCaptcha } = useRecaptcha(); - const { isMobile, challenge } = useMobileAppLogin(); - const { register, handleSubmit, @@ -181,8 +173,6 @@ export function RegisterForm({ search, initialData }: RegisterProps) { error: mobileRegisterError, } = mutations.useMobileSignUp(); - const registerError = webRegisterError || mobileRegisterError; - const handleRegister = async (inputs: RegisterInputs) => { // lowercase email const email = inputs.email.toLowerCase(); @@ -216,8 +206,9 @@ export function RegisterForm({ search, initialData }: RegisterProps) { }); }; - const nameError = errors.name?.message satisfies string | undefined; - const emailError = errors.email?.message satisfies string | undefined; + const nameError = errors.name?.message; + const emailError = errors.email?.message; + const disableRegister = Boolean(Object.keys(errors).length > 0); return ( @@ -258,9 +249,7 @@ export function RegisterForm({ search, initialData }: RegisterProps) { MemberConstants.USERNAME_FORMAT_REGEX.test(name.trim()) || translateCommon('USERNAME_SPECIAL_CHARACTERS'), })} - placeholder={t(NAME_FIELD_LABEL)} - // todo: Should we not allow users to change their name when creating a count from an invitation ? - // disabled={Boolean(invitation?.name)} + placeholder={t('NAME_FIELD_LABEL')} /> - + 0)} + disabled={disableRegister} > - {t(SIGN_UP_BUTTON)} + {t('SIGN_UP_BUTTON')} - {t(SIGN_IN_LINK_TEXT)} + {t('SIGN_IN_LINK_TEXT')} ); diff --git a/src/routes/auth/register.tsx b/src/routes/auth/register.tsx index 93a4562c1..3f3e1cdaa 100644 --- a/src/routes/auth/register.tsx +++ b/src/routes/auth/register.tsx @@ -59,7 +59,7 @@ function RegisterWithInvitation() { {t('INVITATION_NOT_FOUND_MESSAGE')} - + ); } @@ -70,5 +70,5 @@ function RegisterPage() { return ; } - return ; + return ; }