diff --git a/playwright.config.ts b/playwright.config.ts index 5ee376920..c2045c731 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,5 +1,8 @@ import { defineConfig, devices } from '@playwright/test'; +// Set NODE_ENV to test for test environment +process.env.NODE_ENV = 'test'; + /** * Read environment variables from file. * https://github.com/motdotla/dotenv diff --git a/src/common/schema.ts b/src/common/schema.ts index abf873e95..487f56671 100644 --- a/src/common/schema.ts +++ b/src/common/schema.ts @@ -1728,6 +1728,7 @@ export interface components { */ authType: "legacy" | "cognito"; onboarding_pending?: boolean; + invitation_token?: string; }; /** UserPreference */ UserPreference: { diff --git a/src/features/api/index.ts b/src/features/api/index.ts index 76f94db32..46876855e 100644 --- a/src/features/api/index.ts +++ b/src/features/api/index.ts @@ -3302,6 +3302,7 @@ export type User = { unguess_wp_user_id: number; authType: 'legacy' | 'cognito'; onboarding_pending?: boolean; + invitation_token?: string; }; export type UserPreference = { name: string; diff --git a/src/features/auth/context.tsx b/src/features/auth/context.tsx index 5bacf7767..c2edc77bc 100644 --- a/src/features/auth/context.tsx +++ b/src/features/auth/context.tsx @@ -9,6 +9,7 @@ import { resendSignUpCode, resetPassword, confirmResetPassword, + updatePassword, type SignInInput, type SignUpInput, type ConfirmSignUpInput, @@ -24,6 +25,7 @@ interface LoginResult { isSignedIn: boolean; mfaChallenge?: MfaChallengeStep; requiresNewPassword?: boolean; + requiresSignUpConfirmation?: boolean; } interface ForgotPasswordResult { @@ -38,6 +40,7 @@ interface AuthContextType { resendSignupCode: (email: string) => Promise; confirmMfaSignIn: (code: string) => Promise; setNewPassword: (newPassword: string) => Promise; + changePassword: (oldPassword: string, newPassword: string) => Promise; forgotPassword: (email: string) => Promise; confirmForgotPassword: ( email: string, @@ -87,6 +90,14 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ requiresNewPassword: true, }; } + + // Utente registrato ma non ha confermato l'email + if (nextStep.signInStep === 'CONFIRM_SIGN_UP') { + return { + isSignedIn: false, + requiresSignUpConfirmation: true, + }; + } } await syncWordpress(); @@ -129,6 +140,20 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ } }; + const changePassword = async ( + oldPassword: string, + newPassword: string + ): Promise => { + try { + await updatePassword({ oldPassword, newPassword }); + await syncWordpress(); + } catch (error: any) { + // eslint-disable-next-line no-console + console.error('Change password error:', error); + throw new Error(error.message || 'Failed to change password'); + } + }; + const signup = async (email: string, password: string, name: string) => { try { const signUpInput: SignUpInput = { @@ -241,6 +266,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ resendSignupCode, confirmMfaSignIn, setNewPassword, + changePassword, forgotPassword, confirmForgotPassword: confirmForgotPasswordFn, logout, diff --git a/src/features/auth/syncWordpress.ts b/src/features/auth/syncWordpress.ts index e5a778e7c..3ce2eed07 100644 --- a/src/features/auth/syncWordpress.ts +++ b/src/features/auth/syncWordpress.ts @@ -2,16 +2,17 @@ import { fetchAuthSession } from 'aws-amplify/auth'; import { isDev } from 'src/common/isDevEnvironment'; export const syncWordpress = async (): Promise => { - const session = await fetchAuthSession(); + try { + const session = await fetchAuthSession(); - const idToken = session.tokens?.idToken?.toString(); + const idToken = session.tokens?.idToken?.toString(); + + if (!idToken) { + // eslint-disable-next-line no-console + console.error('SyncWP: No ID token available'); + return; + } - if (!idToken) { - // eslint-disable-next-line no-console - console.error('SyncWP: No ID token available'); - return; - } - try { const response = await fetch( `${process.env.REACT_APP_CROWD_WP_URL}/wp-json/cognito-sync/v1/login`, { @@ -23,12 +24,11 @@ export const syncWordpress = async (): Promise => { ); const result = await response.json(); - if (isDev()) { // eslint-disable-next-line no-console console.log('🚀 ~ syncWordpress ~ result:', result); } - } catch (error: any) { + } catch (error) { // eslint-disable-next-line no-console console.error('SyncWP error:', error); } diff --git a/src/features/navigation/Navigation/NavigationProfileModal.tsx b/src/features/navigation/Navigation/NavigationProfileModal.tsx index d8e9544b7..282883442 100644 --- a/src/features/navigation/Navigation/NavigationProfileModal.tsx +++ b/src/features/navigation/Navigation/NavigationProfileModal.tsx @@ -8,8 +8,7 @@ import { useNavigate } from 'react-router-dom'; import { useAppDispatch, useAppSelector } from 'src/app/hooks'; import { isDev } from 'src/common/isDevEnvironment'; import { prepareGravatar } from 'src/common/utils'; -import WPAPI from 'src/common/wpapi'; -import { useGetUsersMeQuery, unguessApi } from 'src/features/api'; +import { useGetUsersMeQuery } from 'src/features/api'; import { useAuth } from 'src/features/auth/context'; import { useActiveWorkspace } from 'src/hooks/useActiveWorkspace'; import { setProfileModalOpen } from '../navigationSlice'; @@ -27,7 +26,6 @@ export const NavigationProfileModal = () => { const { t, i18n } = useTranslation(); const dispatch = useAppDispatch(); const navigate = useNavigate(); - const { logout: cognitoLogout } = useAuth(); const pathWithoutLocale = usePathWithoutLocale(); @@ -102,23 +100,7 @@ export const NavigationProfileModal = () => { } }, onLogout: async () => { - // TODO: rimuovere wp dopo migration completa - try { - // Se l'utente è autenticato con Cognito, esegui logout Cognito - if (user?.authType === 'cognito') { - await cognitoLogout(); - } - - // Logout legacy WP per utenti con cookie - await WPAPI.logout(); - - // Reset rtk query cache - dispatch(unguessApi.util.resetApiState()); - } catch (err) { - // eslint-disable-next-line no-console - console.error('logout error:', err); - document.location.href = '/login'; - } + navigate('/logout'); }, onCopyEmail: () => { addToast( diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index bf7b50dae..02ab91e75 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1631,10 +1631,13 @@ "CONFIRM_EMAIL_DESCRIPTION": "We sent an email with your code to:", "CONFIRM_EMAIL_DIDNT_RECEIVE": "Didn't receive a code?", "CONFIRM_EMAIL_ERROR_GENERIC": "Something went wrong. Please try again.", - "CONFIRM_EMAIL_TITLE": "Verify your email", + "CONFIRM_EMAIL_TITLE": "All set! Just verify your email to begin", "CONFIRM_PASSWORD_LABEL": "Confirm password", + "CONFIRM_PASSWORD_MUST_MATCH": "Passwords must match", "CONFIRM_PASSWORD_PLACEHOLDER": "Confirm your password", + "CONFIRM_PASSWORD_REQUIRED": "Please confirm your password", "FORGOT_PASSWORD_BACK_TO_LOGIN": "Back to log in", + "FORGOT_PASSWORD_BACK_TO_PROFILE": "Back to profile", "FORGOT_PASSWORD_CHANGE_SUBTITLE": "Enter a new password for your account", "FORGOT_PASSWORD_CHANGE_TITLE": "Change your password", "FORGOT_PASSWORD_CHECK_EMAIL_SUBTITLE": "We sent a password reset link to {{email}}.", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index e4bc43fbc..c3d4f9027 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1676,8 +1676,11 @@ "CONFIRM_EMAIL_ERROR_GENERIC": "", "CONFIRM_EMAIL_TITLE": "", "CONFIRM_PASSWORD_LABEL": "", + "CONFIRM_PASSWORD_MUST_MATCH": "", "CONFIRM_PASSWORD_PLACEHOLDER": "", + "CONFIRM_PASSWORD_REQUIRED": "", "FORGOT_PASSWORD_BACK_TO_LOGIN": "", + "FORGOT_PASSWORD_BACK_TO_PROFILE": "", "FORGOT_PASSWORD_CHANGE_SUBTITLE": "", "FORGOT_PASSWORD_CHANGE_TITLE": "", "FORGOT_PASSWORD_CHECK_EMAIL_SUBTITLE": "", diff --git a/src/pages/Auth/logout.tsx b/src/pages/Auth/logout.tsx index 7ade19ba1..d3a6c6a23 100644 --- a/src/pages/Auth/logout.tsx +++ b/src/pages/Auth/logout.tsx @@ -1,13 +1,43 @@ import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; +import { useAppDispatch } from 'src/app/hooks'; import { PageLoader } from 'src/common/components/PageLoader'; +import { unguessApi, useGetUsersMeQuery } from 'src/features/api'; +import { useAuth } from 'src/features/auth/context'; export const LogoutPage = () => { const navigate = useNavigate(); + const { data: user } = useGetUsersMeQuery(); + const { logout: cognitoLogout } = useAuth(); + const dispatch = useAppDispatch(); useEffect(() => { - navigate('/login'); - }, [navigate]); + const performLogout = async () => { + try { + if (user?.authType === 'cognito') { + await cognitoLogout(); + } + } catch (err) { + // eslint-disable-next-line no-console + console.error('Cognito logout error:', err); + } + + try { + await fetch( + `${process.env.REACT_APP_CROWD_WP_URL}/wp-admin/admin-ajax.php?action=unguess_wp_logout`, + { method: 'GET', credentials: 'include' } + ); + } catch (err) { + // eslint-disable-next-line no-console + console.error('WP logout error:', err); + } + + dispatch(unguessApi.util.resetApiState()); + navigate('/login'); + }; + + performLogout(); + }, [navigate, dispatch, user, cognitoLogout]); return ; }; diff --git a/src/pages/ForgotPasswordPage/CheckEmailStep.tsx b/src/pages/ForgotPasswordPage/CheckEmailStep.tsx index 9fd5e7ce9..98d38d3f3 100644 --- a/src/pages/ForgotPasswordPage/CheckEmailStep.tsx +++ b/src/pages/ForgotPasswordPage/CheckEmailStep.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; +import { useSearchParams } from 'react-router-dom'; import { Anchor, MD, Span, Title, XL } from '@appquality/unguess-design-system'; import { appTheme } from 'src/app/theme'; import { useLocalizeRoute } from 'src/hooks/useLocalizedRoute'; @@ -14,7 +15,10 @@ interface CheckEmailStepProps { export const CheckEmailStep = ({ email, onResend }: CheckEmailStepProps) => { const { t } = useTranslation(); + const [searchParams] = useSearchParams(); + const isFromProfile = searchParams.get('from') === 'profile'; const loginRoute = useLocalizeRoute('login'); + const profileRoute = useLocalizeRoute('profile'); const [resendTimer, setResendTimer] = useState(0); const [isResending, setIsResending] = useState(false); @@ -103,8 +107,13 @@ export const CheckEmailStep = ({ email, onResend }: CheckEmailStepProps) => {
- - {t('FORGOT_PASSWORD_BACK_TO_LOGIN')} + + {isFromProfile + ? t('FORGOT_PASSWORD_BACK_TO_PROFILE') + : t('FORGOT_PASSWORD_BACK_TO_LOGIN')}
diff --git a/src/pages/ForgotPasswordPage/ForgotPasswordForm.tsx b/src/pages/ForgotPasswordPage/ForgotPasswordForm.tsx index 2ec1deb48..eb39f6e59 100644 --- a/src/pages/ForgotPasswordPage/ForgotPasswordForm.tsx +++ b/src/pages/ForgotPasswordPage/ForgotPasswordForm.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useSearchParams } from 'react-router-dom'; import { Button, FormField, @@ -26,7 +27,10 @@ interface ForgotPasswordFormProps { export const ForgotPasswordForm = ({ onSubmit }: ForgotPasswordFormProps) => { const { t } = useTranslation(); + const [searchParams] = useSearchParams(); + const isFromProfile = searchParams.get('from') === 'profile'; const loginRoute = useLocalizeRoute('login'); + const profileRoute = useLocalizeRoute('profile'); const [email, setEmail] = useState(''); const [error, setError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); @@ -106,13 +110,15 @@ export const ForgotPasswordForm = ({ onSubmit }: ForgotPasswordFormProps) => { diff --git a/src/pages/JoinPage/InvitedUserPage/SetPasswordForm.tsx b/src/pages/JoinPage/InvitedUserPage/SetPasswordForm.tsx index 9f0fbe5a1..24343a9fd 100644 --- a/src/pages/JoinPage/InvitedUserPage/SetPasswordForm.tsx +++ b/src/pages/JoinPage/InvitedUserPage/SetPasswordForm.tsx @@ -1,11 +1,14 @@ import { + Anchor, Button, + Checkbox, FormField, Input, Label, MediaInput, Message, Paragraph, + SM, Span, XL, } from '@appquality/unguess-design-system'; @@ -13,15 +16,19 @@ import { ReactComponent as Eye } from '@zendeskgarden/svg-icons/src/16/eye-fill. import { ReactComponent as EyeHide } from '@zendeskgarden/svg-icons/src/16/eye-hide-fill.svg'; import { Field, FieldProps, Form, Formik, FormikHelpers } from 'formik'; import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { appTheme } from 'src/app/theme'; import { PasswordRequirements } from 'src/common/components/PasswordRequirements'; import { useAuth } from 'src/features/auth/context'; import { useSendGTMevent } from 'src/hooks/useGTMevent'; import styled from 'styled-components'; -import { GetInvitesByProfileAndTokenApiResponse } from 'src/features/api'; -import { setPasswordValidationSchema } from './validationSchema'; +import { + GetInvitesByProfileAndTokenApiResponse, + unguessApi, +} from 'src/features/api'; +import { useAppDispatch } from 'src/app/hooks'; +import { getSetPasswordValidationSchema } from './validationSchema'; const FieldContainer = styled.div` display: flex; @@ -32,6 +39,8 @@ const FieldContainer = styled.div` interface SetPasswordFormValues { password: string; confirmPassword: string; + termsAccepted: boolean; + privacyAccepted: boolean; } interface SetPasswordFormProps { @@ -46,7 +55,8 @@ export const SetPasswordForm = ({ token, }: SetPasswordFormProps) => { const { t } = useTranslation(); - const { signup, login, setNewPassword } = useAuth(); + const { signup, login, setNewPassword, changePassword } = useAuth(); + const dispatch = useAppDispatch(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const sendGTMevent = useSendGTMevent({ loggedUser: false }); @@ -80,12 +90,31 @@ export const SetPasswordForm = ({ throw new Error('Email not found in invite data'); } - // Uso la temp password per autenticare l'utente + // Uso la password permanente per autenticare l'utente if (inviteData?.code) { - // 1. Login con password temporanea + // 1. Login con password permanente const loginResult = await login(email, inviteData.code); - // 2. Se richiede cambio password, imposta la nuova + // Se il login è riuscito direttamente (utente già confermato) + if (loginResult.isSignedIn) { + // 2. Cambia la password dalla password permanente a quella scelta dall'utente + await changePassword(inviteData.code, values.password); + + sendGTMevent({ + event: 'sign-up-flow', + category: 'invited', + action: 'password-changed-completed', + }); + + sessionStorage.setItem('inviteProfileId', profileId.toString()); + sessionStorage.setItem('inviteToken', token); + await dispatch(unguessApi.util.invalidateTags(['Users'])); + const queryString = searchParams.toString(); + navigate(`/join/onboarding${queryString ? `?${queryString}` : ''}`); + + return; + } + if (loginResult.requiresNewPassword) { await setNewPassword(values.password); @@ -94,17 +123,19 @@ export const SetPasswordForm = ({ category: 'invited', action: 'password-changed-completed', }); - } - // 3. L'utente è ora loggato, salva i dati dell'invito e vai all'onboarding - sessionStorage.setItem('inviteProfileId', profileId.toString()); - sessionStorage.setItem('inviteToken', token); + sessionStorage.setItem('inviteProfileId', profileId.toString()); + sessionStorage.setItem('inviteToken', token); + await dispatch(unguessApi.util.invalidateTags(['Users'])); + const queryString = searchParams.toString(); + navigate(`/join/onboarding${queryString ? `?${queryString}` : ''}`); - // Preserva i query params durante la navigazione - const queryString = searchParams.toString(); - navigate(`/join/onboarding${queryString ? `?${queryString}` : ''}`); + return; + } - return; + throw new Error( + `Unexpected sign-in result: ${JSON.stringify(loginResult)}` + ); } // VECCHIO FLUSSO (backward compatibility): Signup tradizionale @@ -129,7 +160,7 @@ export const SetPasswordForm = ({ sessionStorage.setItem('inviteProfileId', profileId.toString()); sessionStorage.setItem('inviteToken', token); - // Preserva i query params durante la navigazione + await dispatch(unguessApi.util.invalidateTags(['Users'])); const queryString = searchParams.toString(); navigate(`/join/onboarding${queryString ? `?${queryString}` : ''}`); } catch (loginError: any) { @@ -166,6 +197,8 @@ export const SetPasswordForm = ({ const initialValues: SetPasswordFormValues = { password: '', confirmPassword: '', + termsAccepted: false, + privacyAccepted: false, }; return ( @@ -196,7 +229,7 @@ export const SetPasswordForm = ({ {({ isSubmitting, values }) => ( @@ -297,7 +330,103 @@ export const SetPasswordForm = ({ }} - diff --git a/src/pages/JoinPage/InvitedUserPage/validationSchema.ts b/src/pages/JoinPage/InvitedUserPage/validationSchema.ts index 14ffcae61..1bf17fa56 100644 --- a/src/pages/JoinPage/InvitedUserPage/validationSchema.ts +++ b/src/pages/JoinPage/InvitedUserPage/validationSchema.ts @@ -1,13 +1,30 @@ +import { TFunction } from 'i18next'; import * as Yup from 'yup'; -export const setPasswordValidationSchema = Yup.object().shape({ - password: Yup.string() - .min(12, 'SIGNUP_FORM_PASSWORD_MIN_LENGTH') - .matches(/[a-z]/, 'SIGNUP_FORM_PASSWORD_LOWERCASE') - .matches(/[A-Z]/, 'SIGNUP_FORM_PASSWORD_UPPERCASE') - .matches(/[0-9]/, 'SIGNUP_FORM_PASSWORD_NUMBER') - .required('SIGNUP_FORM_PASSWORD_REQUIRED'), - confirmPassword: Yup.string() - .oneOf([Yup.ref('password')], 'CONFIRM_PASSWORD_MUST_MATCH') - .required('CONFIRM_PASSWORD_REQUIRED'), -}); +export const getSetPasswordValidationSchema = (t: TFunction) => + Yup.object().shape({ + password: Yup.string() + .min(12, t('SIGNUP_FORM_PASSWORD_MUST_BE_AT_LEAST_12_CHARACTER_LONG')) + .matches( + /[a-z]/, + t('SIGNUP_FORM_PASSWORD_MUST_CONTAIN_AT_LEAST_A_LOWERCASE_LETTER') + ) + .matches( + /[A-Z]/, + t('SIGNUP_FORM_PASSWORD_MUST_CONTAIN_AT_LEAST_AN_UPPERCASE_LETTER') + ) + .matches( + /[0-9]/, + t('SIGNUP_FORM_PASSWORD_MUST_CONTAIN_AT_LEAST_A_NUMBER') + ) + .required(t('SIGNUP_FORM_PASSWORD_REQUIRED')), + confirmPassword: Yup.string() + .oneOf([Yup.ref('password')], t('CONFIRM_PASSWORD_MUST_MATCH')) + .required(t('CONFIRM_PASSWORD_REQUIRED')), + termsAccepted: Yup.boolean() + .oneOf([true], t('SIGNUP_FORM_TERMS_REQUIRED')) + .required(t('SIGNUP_FORM_TERMS_REQUIRED')), + privacyAccepted: Yup.boolean() + .oneOf([true], t('SIGNUP_FORM_PRIVACY_REQUIRED')) + .required(t('SIGNUP_FORM_PRIVACY_REQUIRED')), + }); diff --git a/src/pages/JoinPage/OnboardingPage/Steps/PersonalInfoStep.tsx b/src/pages/JoinPage/OnboardingPage/Steps/PersonalInfoStep.tsx index 8806e6dc6..c16788364 100644 --- a/src/pages/JoinPage/OnboardingPage/Steps/PersonalInfoStep.tsx +++ b/src/pages/JoinPage/OnboardingPage/Steps/PersonalInfoStep.tsx @@ -19,7 +19,7 @@ import { } from 'formik'; import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; +import { useSearchParams } from 'react-router-dom'; import { appTheme } from 'src/app/theme'; import { useGetUsersRolesQuery, @@ -116,8 +116,8 @@ const AutofillSync = () => { export const PersonalInfoStep = () => { const { t } = useTranslation(); - const { data, userData, updateData, setStep, queryParams } = useOnboarding(); - const navigate = useNavigate(); + const { data, userData, updateData, setStep } = useOnboarding(); + const [searchParams] = useSearchParams(); const sendGTMevent = useSendGTMevent({ loggedUser: false }); const [postUsers] = usePostUsersMutation(); const { data: dataRoles, isLoading: isLoadingRoles } = @@ -127,8 +127,9 @@ export const PersonalInfoStep = () => { const selectRef = useRef(null); const roleSelectRef = useRef(null); - // Usa il templateId dai queryParams del provider - const templateId = queryParams.template; + const templateParam = searchParams.get('template'); + const templateId = templateParam !== null ? Number(templateParam) : undefined; + const redirectTo = searchParams.get('redirect') || undefined; const renderRolesOptions = useMemo( () => @@ -214,11 +215,10 @@ export const PersonalInfoStep = () => { sessionStorage.removeItem('inviteProfileId'); sessionStorage.removeItem('inviteToken'); - // Redirect - if (res.projectId) { - navigate(`/projects/${res.projectId}`); + if (redirectTo) { + window.location.href = redirectTo; } else { - navigate('/'); + window.location.href = '/'; } } else { // Utente nuovo: vai allo step 2 (workspace) @@ -262,9 +262,14 @@ export const PersonalInfoStep = () => { initialValues={initialValues} validationSchema={getPersonalInfoValidationSchema(t)} onSubmit={handleSubmit} - validateOnMount > - {({ isSubmitting, isValid, setFieldValue }) => ( + {({ + isSubmitting, + values, + setFieldValue, + validateForm, + setTouched, + }) => (
@@ -437,7 +442,27 @@ export const PersonalInfoStep = () => { isAccent isStretched size="medium" - disabled={isSubmitting || !isValid} + disabled={ + isSubmitting || + !values.name || + !values.surname || + !values.roleId || + !values.companySizeId + } + onClick={async (e) => { + e.preventDefault(); + await setTouched({ + name: true, + surname: true, + roleId: true, + companySizeId: true, + }); + const formErrors = await validateForm(); + if (Object.keys(formErrors).length === 0) { + const form = (e.target as HTMLElement).closest('form'); + form?.requestSubmit(); + } + }} > {isSubmitting ? t('LOADING') : t('SIGNUP_FORM_NEXT_STEP')} diff --git a/src/pages/JoinPage/OnboardingPage/Steps/WorkspaceStep.tsx b/src/pages/JoinPage/OnboardingPage/Steps/WorkspaceStep.tsx index ac6251df1..b05ac62ee 100644 --- a/src/pages/JoinPage/OnboardingPage/Steps/WorkspaceStep.tsx +++ b/src/pages/JoinPage/OnboardingPage/Steps/WorkspaceStep.tsx @@ -10,7 +10,7 @@ import { } from '@appquality/unguess-design-system'; import { Field, FieldProps, Form, Formik, FormikHelpers } from 'formik'; import { useTranslation } from 'react-i18next'; - +import { useSearchParams } from 'react-router-dom'; import { appTheme } from 'src/app/theme'; import { usePostUsersMutation } from 'src/features/api'; import { useSendGTMevent } from 'src/hooks/useGTMevent'; @@ -40,11 +40,12 @@ interface WorkspaceFormValues { export const WorkspaceStep = () => { const { t } = useTranslation(); - const { data, userData, updateData, setStep, queryParams } = useOnboarding(); + const { data, userData, updateData, setStep } = useOnboarding(); + const [searchParams] = useSearchParams(); const sendGTMevent = useSendGTMevent({ loggedUser: false }); const [postUsers] = usePostUsersMutation(); - const templateId = queryParams.template; - const redirectTo = queryParams.redirect as string | undefined; + const templateParam = searchParams.get('template'); + const templateId = templateParam !== null ? Number(templateParam) : undefined; const handleSubmit = async ( values: WorkspaceFormValues, @@ -100,7 +101,7 @@ export const WorkspaceStep = () => { email: userData.email!, firstName: data.name, lastName: data.surname, - searchParams: queryParams, + searchParams: Object.fromEntries(searchParams.entries()), }); } catch (err) { console.error('Error sending data to HubSpot:', err); @@ -112,9 +113,11 @@ export const WorkspaceStep = () => { sessionStorage.removeItem('inviteToken'); } - // Redirect appropriato (utente già loggato) - const redirectTarget = res.projectId ? `/projects/${res.projectId}` : '/'; - window.location.href = redirectTo || redirectTarget; + if (res.projectId) { + window.location.href = `/projects/${res.projectId}`; + } else { + window.location.href = '/'; + } } catch (error: any) { // eslint-disable-next-line no-console console.error('Onboarding save error:', error); diff --git a/src/pages/JoinPage/OnboardingPage/index.tsx b/src/pages/JoinPage/OnboardingPage/index.tsx index bfb757c42..e78395d15 100644 --- a/src/pages/JoinPage/OnboardingPage/index.tsx +++ b/src/pages/JoinPage/OnboardingPage/index.tsx @@ -108,8 +108,11 @@ const OnboardingPage = () => { let userData: OnboardingUserData | undefined; if (currentUser) { // Controlla se è un utente invitato (profileId e token in sessionStorage) - const inviteProfileId = sessionStorage.getItem('inviteProfileId'); - const inviteToken = sessionStorage.getItem('inviteToken'); + const inviteProfileId = + sessionStorage.getItem('inviteProfileId') || + currentUser.profile_id?.toString(); + const inviteToken = + sessionStorage.getItem('inviteToken') || currentUser.invitation_token; if (inviteProfileId && inviteToken) { // Utente invitato diff --git a/src/pages/JoinPage/SignupPage/ConfirmEmailForm.tsx b/src/pages/JoinPage/SignupPage/ConfirmEmailForm.tsx index 19209cf44..1d9b48586 100644 --- a/src/pages/JoinPage/SignupPage/ConfirmEmailForm.tsx +++ b/src/pages/JoinPage/SignupPage/ConfirmEmailForm.tsx @@ -176,14 +176,6 @@ export const ConfirmEmailForm = ({ > {isVerifying ? t('LOADING') : t('CONFIRM_EMAIL_BUTTON')} - ); }; diff --git a/src/pages/JoinPage/SignupPage/index.tsx b/src/pages/JoinPage/SignupPage/index.tsx index 545137e82..f2e5d1f36 100644 --- a/src/pages/JoinPage/SignupPage/index.tsx +++ b/src/pages/JoinPage/SignupPage/index.tsx @@ -5,6 +5,7 @@ */ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useLocation } from 'react-router-dom'; import { LayoutWrapper } from 'src/common/components/LayoutWrapper'; import joinBg from 'src/assets/join-bg-1.png'; import joingBgwebp from 'src/assets/join-bg-1.webp'; @@ -70,11 +71,22 @@ const RightColumn = styled.div` } `; +interface SignupLocationState { + email?: string; + password?: string; + needsConfirmation?: boolean; +} + const SignupPage = () => { const { t } = useTranslation(); - const [userEmail, setUserEmail] = useState(''); - const [userPassword, setUserPassword] = useState(''); - const [needsConfirmation, setNeedsConfirmation] = useState(false); + const { state: locationState } = useLocation(); + const navState = locationState as SignupLocationState | null; + + const [userEmail, setUserEmail] = useState(navState?.email || ''); + const [userPassword, setUserPassword] = useState(navState?.password || ''); + const [needsConfirmation, setNeedsConfirmation] = useState( + navState?.needsConfirmation || false + ); const meta = [ { name: 'og:description', content: t('__PAGE_JOIN_DESCRIPTION') }, diff --git a/src/pages/LoginPage/index.tsx b/src/pages/LoginPage/index.tsx index 9e24fe4d0..be231686e 100644 --- a/src/pages/LoginPage/index.tsx +++ b/src/pages/LoginPage/index.tsx @@ -52,6 +52,7 @@ const LoginPage = () => { const { addToast } = useToast(); const { login: cognitoLogin } = useAuth(); const verifyCodeRoute = useLocalizeRoute('verify-code'); + const signupRoute = useLocalizeRoute('join/signup'); const from = (locationState as NavigationState)?.from || '/'; const [searchParams] = useSearchParams(); @@ -113,6 +114,17 @@ const LoginPage = () => { return; } + if (result.requiresSignUpConfirmation) { + navigate(signupRoute, { + state: { + email: values.email, + password: values.password, + needsConfirmation: true, + }, + }); + return; + } + // Login Cognito riuscito setCta(`${t('__LOGIN_FORM_CTA_REDIRECT_STATE')}`); document.location.href = from || '/'; diff --git a/src/pages/Plan/Controls/index.tsx b/src/pages/Plan/Controls/index.tsx index dc2156f27..94f7f2f0a 100644 --- a/src/pages/Plan/Controls/index.tsx +++ b/src/pages/Plan/Controls/index.tsx @@ -21,9 +21,12 @@ import { SaveConfigurationButton } from './SaveConfigurationButton'; import { WatcherList } from './WatcherList'; const VerticalDivider = styled.span` - border-left: 1px solid ${({ theme }) => theme.palette.grey['300']}; - height: ${({ theme }) => theme.space.md}; - width: 1px; + display: none; + @media (min-width: ${appTheme.breakpoints.sm}) { + border-left: 1px solid ${({ theme }) => theme.palette.grey['300']}; + height: ${({ theme }) => theme.space.md}; + width: 1px; + } `; export const Controls = () => { diff --git a/src/pages/Plan/modules/Factory/modules/Tasks/Component/parts/modal/CreateVideoTasksWithAI.tsx b/src/pages/Plan/modules/Factory/modules/Tasks/Component/parts/modal/CreateVideoTasksWithAI.tsx index 896eb8d56..7c638e3a7 100644 --- a/src/pages/Plan/modules/Factory/modules/Tasks/Component/parts/modal/CreateVideoTasksWithAI.tsx +++ b/src/pages/Plan/modules/Factory/modules/Tasks/Component/parts/modal/CreateVideoTasksWithAI.tsx @@ -217,10 +217,18 @@ const CreateVideoTasksWithAI = () => { } - onSelect={(value) => setUsecaseNumber(Number(value))} + onSelect={(value) => + setUsecaseNumber(value === 'auto' ? undefined : Number(value)) + } isDisabled={isPostingRequest} style={{ maxWidth: '150px' }} > + {[1, 2, 3, 4, 5].map((item) => ( { const isCognitoUser = userData.authType === 'cognito'; if (isCognitoUser) { + // Refresh della sessione per assicurarsi che l'access token sia valido + await fetchAuthSession({ forceRefresh: true }); await updatePassword({ oldPassword: values.currentPassword, newPassword: values.newPassword, diff --git a/src/pages/Profile/parts/PasswordAccordion/CurrentPassword.tsx b/src/pages/Profile/parts/PasswordAccordion/CurrentPassword.tsx index a11ce2bea..d85d64531 100644 --- a/src/pages/Profile/parts/PasswordAccordion/CurrentPassword.tsx +++ b/src/pages/Profile/parts/PasswordAccordion/CurrentPassword.tsx @@ -74,7 +74,7 @@ const CurrentPassword = () => { {t('__PAGE_PROFILE_FORGOT_PASSWORD')} diff --git a/tests/fixtures/pages/Join/InvitedUserPage.ts b/tests/fixtures/pages/Join/InvitedUserPage.ts index 85169913a..c15acb764 100644 --- a/tests/fixtures/pages/Join/InvitedUserPage.ts +++ b/tests/fixtures/pages/Join/InvitedUserPage.ts @@ -39,6 +39,10 @@ export class InvitedUserPage { this.page.getByTestId('confirm-password-error'), passwordRequirements: () => this.page.getByTestId('password-requirements'), + termsCheckbox: () => + this.page.getByTestId('terms-and-conditions').locator('input'), + privacyCheckbox: () => + this.page.getByTestId('privacy-policy').locator('input'), submitButton: () => this.page.getByRole('button', { name: this.i18n.t('SET_PASSWORD_BUTTON'), @@ -58,9 +62,15 @@ export class InvitedUserPage { await confirmPasswordInput.blur(); } + async acceptTermsAndPrivacy() { + await this.elements().termsCheckbox().check({ force: true }); + await this.elements().privacyCheckbox().check({ force: true }); + } + async fillValidPasswordForm() { await this.fillPassword('ValidPassword123'); await this.fillConfirmPassword('ValidPassword123'); + await this.acceptTermsAndPrivacy(); } async submitForm() {