diff --git a/src/App.tsx b/src/App.tsx index 08cd435bc0..d2afa739df 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,6 +24,9 @@ import localStorageService from 'services/local-storage.service'; import navigationService from 'services/navigation.service'; import { AppViewConfig } from './app/core/types'; +import AuthShell from './components/AuthShell/AuthShell'; +import { LogIn } from './views/Login/components'; +import SignUpForm from './views/Signup/components/SignupForm'; import { LRUFilesCacheManager } from './app/database/services/database.service/LRUFilesCacheManager'; import { LRUFilesPreviewCacheManager } from './app/database/services/database.service/LRUFilesPreviewCacheManager'; import FileViewerWrapper from './app/drive/components/FileViewer/FileViewerWrapper'; @@ -203,6 +206,18 @@ const App = (props: AppProps): JSX.Element => { to={`/?preferences=open§ion=account&subsection=${params.get('tab') ?? 'account'}`} /> + ( + + + + + + + )} + /> {!MOBILE_EXCLUDED_PATHS.includes(pathName) && isMobile && isAuthenticated ? ( diff --git a/src/app/i18n/locales/de.json b/src/app/i18n/locales/de.json index c8a490c84c..2ccaf55fb6 100644 --- a/src/app/i18n/locales/de.json +++ b/src/app/i18n/locales/de.json @@ -77,7 +77,9 @@ "environment": "Weltumwelt" }, "auth": { + "emailFloatingLabel": "E-Mail-Adresse", "email": "E-Mail", + "passwordFloatingLabel": "Passwort", "password": "Passwort", "terms1": "Durch die Erstellung eines Kontos,", "terms2": "Sie akzeptieren die Geschäftsbedingungen", diff --git a/src/app/i18n/locales/en.json b/src/app/i18n/locales/en.json index e6bdac60ac..cb38d0354b 100644 --- a/src/app/i18n/locales/en.json +++ b/src/app/i18n/locales/en.json @@ -98,14 +98,16 @@ "environment": "World Environment Day" }, "auth": { - "email": "Email", + "emailFloatingLabel": "Email address", + "email": "Email address", + "passwordFloatingLabel": "Password", "password": "Password", "terms1": "By creating an account, ", "terms2": "you accept the terms & conditions", "decrypting": "Decrypting...", "login": { - "title": "Login", - "forgotPwd": "Forgot your password?", + "title": "Log in", + "forgotPwd": "I forgot my password", "dontHaveAccount": "Don’t have an account?", "dontHaveAccountMobile": "Don't have an Internxt account?", "createAccount": "Create account", diff --git a/src/app/i18n/locales/es.json b/src/app/i18n/locales/es.json index 614b802586..4ab4fb4f1a 100644 --- a/src/app/i18n/locales/es.json +++ b/src/app/i18n/locales/es.json @@ -77,7 +77,9 @@ "environment": "Día Mundial del Medio Ambiente" }, "auth": { + "emailFloatingLabel": "Correo electrónico", "email": "Correo electrónico", + "passwordFloatingLabel": "Contraseña", "password": "Contraseña", "terms1": "Al crear una cuenta, ", "terms2": "aceptas los términos y condiciones", diff --git a/src/app/i18n/locales/fr.json b/src/app/i18n/locales/fr.json index c07f2a3112..e483889aa0 100644 --- a/src/app/i18n/locales/fr.json +++ b/src/app/i18n/locales/fr.json @@ -77,7 +77,9 @@ "environment": "Journée Mondiale de l'Environnement" }, "auth": { + "emailFloatingLabel": "Adresse e-mail", "email": "Email", + "passwordFloatingLabel": "Mot de passe", "password": " Mot de passe ", "terms1": "En créant un compte,", "terms2": "vous acceptez les termes et conditions", diff --git a/src/app/i18n/locales/it.json b/src/app/i18n/locales/it.json index 84e533b89b..69896a8beb 100644 --- a/src/app/i18n/locales/it.json +++ b/src/app/i18n/locales/it.json @@ -98,7 +98,9 @@ "environment": "Giornata Mondiale dell'Ambiente" }, "auth": { + "emailFloatingLabel": "Indirizzo email", "email": "Email", + "passwordFloatingLabel": "Password", "password": "Password", "terms1": "Creando un account, ", "terms2": "accetti i termini e le condizioni", diff --git a/src/app/i18n/locales/ru.json b/src/app/i18n/locales/ru.json index 5b509033e8..93b4c0ea34 100644 --- a/src/app/i18n/locales/ru.json +++ b/src/app/i18n/locales/ru.json @@ -77,7 +77,9 @@ "environment": "World Environment Day" }, "auth": { + "emailFloatingLabel": "Электронная почта", "email": "Email", + "passwordFloatingLabel": "Пароль", "password": "Password", "terms1": "При создании учетной записи", "terms2": "вы принимаете условия и положения", diff --git a/src/app/i18n/locales/tw.json b/src/app/i18n/locales/tw.json index e547d990b0..d9cc12c594 100644 --- a/src/app/i18n/locales/tw.json +++ b/src/app/i18n/locales/tw.json @@ -97,7 +97,9 @@ "idManagement": "Identity Management" }, "auth": { + "emailFloatingLabel": "電子郵件", "email": "電子郵件", + "passwordFloatingLabel": "密碼", "password": "密碼", "terms1": "通過創建帳戶,", "terms2": "您接受條款和條件", diff --git a/src/app/i18n/locales/zh.json b/src/app/i18n/locales/zh.json index 993a677be8..f118ec1688 100644 --- a/src/app/i18n/locales/zh.json +++ b/src/app/i18n/locales/zh.json @@ -95,7 +95,9 @@ "anniversary": "Anniversary" }, "auth": { + "emailFloatingLabel": "电子邮件", "email": "电子邮件", + "passwordFloatingLabel": "密码", "password": "密码", "terms1": "创建账户后,", "terms2": "您接受条款和条件", diff --git a/src/components/AnimatedBackground.tsx b/src/components/AnimatedBackground.tsx new file mode 100644 index 0000000000..7560ef591c --- /dev/null +++ b/src/components/AnimatedBackground.tsx @@ -0,0 +1,141 @@ +export default function AnimatedBackground(): JSX.Element { + return ( + <> + + + {/* Light mode orbs */} +
+
+
+
+ + {/* Dark mode orbs */} +
+
+
+
+ + ); +} diff --git a/src/components/AuthShell/AuthShell.tsx b/src/components/AuthShell/AuthShell.tsx new file mode 100644 index 0000000000..beab23be6d --- /dev/null +++ b/src/components/AuthShell/AuthShell.tsx @@ -0,0 +1,36 @@ +import { ReactNode } from 'react'; +import { isMobile } from 'react-device-detect'; +import InternxtLogo from 'assets/icons/big-logo.svg?react'; +import AnimatedBackground from 'components/AnimatedBackground'; +import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; + +interface AuthShellProps { + readonly children: ReactNode; +} + +export default function AuthShell({ children }: AuthShellProps): JSX.Element { + const { translate } = useTranslationContext(); + + return ( +
+ + +
+ +
+ +
{children}
+ + +
+ ); +} diff --git a/src/components/PasswordInput.scss b/src/components/PasswordInput.scss index 4914e080ef..8a1c2372bd 100644 --- a/src/components/PasswordInput.scss +++ b/src/components/PasswordInput.scss @@ -8,6 +8,6 @@ } .input-error { - @apply h-11 w-full rounded-md border bg-transparent text-lg font-normal text-gray-80 outline-none ring-opacity-10 focus:ring-3 disabled:text-gray-40 disabled:placeholder-gray-20 dark:ring-opacity-20 ring-red disabled:border-gray-10 border-gray-40 placeholder-gray-30 px-4 focus:border-red; + @apply h-11 w-full rounded-md border bg-transparent text-lg font-normal text-gray-80 outline-none ring-opacity-10 focus:ring-3 disabled:text-gray-40 disabled:placeholder-gray-20 dark:ring-opacity-20 ring-red disabled:border-gray-10 border-red placeholder-gray-30 px-4 focus:border-red; } } diff --git a/src/components/TextInput.scss b/src/components/TextInput.scss index 4914e080ef..8a1c2372bd 100644 --- a/src/components/TextInput.scss +++ b/src/components/TextInput.scss @@ -8,6 +8,6 @@ } .input-error { - @apply h-11 w-full rounded-md border bg-transparent text-lg font-normal text-gray-80 outline-none ring-opacity-10 focus:ring-3 disabled:text-gray-40 disabled:placeholder-gray-20 dark:ring-opacity-20 ring-red disabled:border-gray-10 border-gray-40 placeholder-gray-30 px-4 focus:border-red; + @apply h-11 w-full rounded-md border bg-transparent text-lg font-normal text-gray-80 outline-none ring-opacity-10 focus:ring-3 disabled:text-gray-40 disabled:placeholder-gray-20 dark:ring-opacity-20 ring-red disabled:border-gray-10 border-red placeholder-gray-30 px-4 focus:border-red; } } diff --git a/src/components/TextInput.tsx b/src/components/TextInput.tsx index b58869cee5..e8de42e408 100644 --- a/src/components/TextInput.tsx +++ b/src/components/TextInput.tsx @@ -25,6 +25,7 @@ interface InputProps { onKeyDown?: (e: React.KeyboardEvent) => void; value?: string; style?: React.CSSProperties; + hasError?: boolean; } export default function TextInput({ @@ -48,6 +49,7 @@ export default function TextInput({ onKeyDown, value, style, + hasError, }: Readonly): JSX.Element { return (
@@ -71,7 +73,7 @@ export default function TextInput({ pattern, }) : { value, onChange, onKeyDown })} - className={`${error ? 'inxt-input input-error' : 'inxt-input input-primary'} ${inputClassName || ''}`} + className={`${error || hasError ? 'inxt-input input-error' : 'inxt-input input-primary'} ${inputClassName || ''}`} />
); diff --git a/src/views/Login/RecoveryLinkView.tsx b/src/views/Login/RecoveryLinkView.tsx index 9bea259188..d62e034008 100644 --- a/src/views/Login/RecoveryLinkView.tsx +++ b/src/views/Login/RecoveryLinkView.tsx @@ -1,21 +1,24 @@ import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; import InternxtLogo from 'assets/icons/big-logo.svg?react'; +import AnimatedBackground from 'components/AnimatedBackground'; import { isMobile } from 'react-device-detect'; import { RecoveryLink } from './components'; function RecoveryLinkView(): JSX.Element { const { translate } = useTranslationContext(); return ( -
-
+
+ + +
-
+
-
+
{!isMobile && ( {translate('general.terms')} diff --git a/src/views/Login/SignInView.tsx b/src/views/Login/SignInView.tsx index 3f574bbaa4..5f5d4feae7 100644 --- a/src/views/Login/SignInView.tsx +++ b/src/views/Login/SignInView.tsx @@ -1,5 +1,6 @@ import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; import InternxtLogo from 'assets/icons/big-logo.svg?react'; +import AnimatedBackground from 'components/AnimatedBackground'; import { isMobile } from 'react-device-detect'; import { LogIn } from './components'; @@ -11,19 +12,23 @@ export default function SignInView(props: Readonly): JSX.Element { const { translate } = useTranslationContext(); return ( -
+
+ + {!props.displayIframe && ( -
+
)} -
+
{!props.displayIframe && ( -
+
{!isMobile && ( {translate('general.terms')} diff --git a/src/views/Login/UniversalLinkSuccessView.tsx b/src/views/Login/UniversalLinkSuccessView.tsx index 5ab3a8c4d4..f17fd9f3da 100644 --- a/src/views/Login/UniversalLinkSuccessView.tsx +++ b/src/views/Login/UniversalLinkSuccessView.tsx @@ -3,6 +3,7 @@ import { Button } from '@internxt/ui'; import { AppView, LocalStorageItem } from 'app/core/types'; import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; import InternxtLogo from 'assets/icons/big-logo.svg?react'; +import AnimatedBackground from 'components/AnimatedBackground'; import { isMobile } from 'react-device-detect'; import { useEffect, useMemo } from 'react'; import authService from 'services/auth.service'; @@ -53,12 +54,14 @@ export default function UniversalLinkView(): JSX.Element { }; return ( -
-
+
+ + +
-
-
+
+

{translate('auth.universalLink.loginAs')}

-
+
{!isMobile && ( {translate('general.terms')} diff --git a/src/views/Login/components/LogIn.tsx b/src/views/Login/components/LogIn.tsx index 66843a1137..e6c47dbee3 100644 --- a/src/views/Login/components/LogIn.tsx +++ b/src/views/Login/components/LogIn.tsx @@ -216,16 +216,20 @@ export default function LogIn(): JSX.Element { }; return ( - <> +
-
-

+
+

{translate('auth.login.title')}

+

{translate('auth.emailFloatingLabel')}

0} /> - +

{translate('auth.passwordFloatingLabel')}

0} /> {showTwoFactor && ( @@ -271,7 +277,7 @@ export default function LogIn(): JSX.Element { loading={isLoggingIn} buttonDataCy="loginButton" variant="primary" - disabled={isLoggingIn} + className="disabled:!bg-primary disabled:!text-white" > {isLoggingIn && isValid ? translate('auth.decrypting') : translate('auth.button.loginAction')} @@ -313,6 +319,6 @@ export default function LogIn(): JSX.Element {

- +
); } diff --git a/src/views/Login/components/RecoveryLink.tsx b/src/views/Login/components/RecoveryLink.tsx index 165ea6b769..879d051b98 100644 --- a/src/views/Login/components/RecoveryLink.tsx +++ b/src/views/Login/components/RecoveryLink.tsx @@ -36,7 +36,10 @@ function RecoveryLink(): JSX.Element { return ( <> {step === 1 ? ( -
+

{translate('auth.forgotPassword.title')}

): JSX.Elemen const isRegularSignup = !props.displayIframe && !autoSubmit.enabled; return ( -
+
+ + {isRegularSignup && ( -
+
)} @@ -33,7 +36,7 @@ export default function SignUpView(props: Readonly): JSX.Elemen
{isRegularSignup && ( -
+
{!isMobile && ( {translate('general.terms')} diff --git a/src/views/Signup/components/SignupForm.tsx b/src/views/Signup/components/SignupForm.tsx index d390abbcc9..7ffc5a636d 100644 --- a/src/views/Signup/components/SignupForm.tsx +++ b/src/views/Signup/components/SignupForm.tsx @@ -245,10 +245,11 @@ function SignUpForm(): JSX.Element { } return ( -
+

{translate('auth.signup.title')}

+

{translate('auth.emailFloatingLabel')}

- +

{translate('auth.passwordFloatingLabel')}

+
@@ -337,7 +341,7 @@ function SignUpForm(): JSX.Element { > {renderContent()}
- +
); } diff --git a/test/e2e/tests/helper/staticData.ts b/test/e2e/tests/helper/staticData.ts index e5641e9358..7899eb237f 100644 --- a/test/e2e/tests/helper/staticData.ts +++ b/test/e2e/tests/helper/staticData.ts @@ -23,7 +23,7 @@ export const staticData = { termsOfServiceTitle: 'Terms of Service', needHelpTitle: 'How can we help you?', howToCreateBackUpKeyPageTitle: 'How do I create a backup key?', - logInPageTitle: 'Login', + logInPageTitle: 'Log in', logInButtonText: 'Log in', userAlreadyRegistered: 'already registered', diff --git a/test/e2e/tests/pages/loginPage.ts b/test/e2e/tests/pages/loginPage.ts index 3e2d28f74d..204ac15e2e 100644 --- a/test/e2e/tests/pages/loginPage.ts +++ b/test/e2e/tests/pages/loginPage.ts @@ -23,12 +23,12 @@ export class LoginPage { constructor(page: Page) { this.page = page; - this.loginTitle = this.page.getByRole('heading', { name: 'Login' }); - this.emailInput = this.page.getByPlaceholder('Email', { exact: true }); + this.loginTitle = this.page.getByRole('heading', { name: 'Log in' }); + this.emailInput = this.page.getByPlaceholder('Email address', { exact: true }); this.passwordInput = this.page.getByPlaceholder('Password', { exact: true }); this.loginButton = this.page.getByRole('button', { name: 'Log in' }); this.loginButtonText = this.page.locator('[data-cy="loginButton"] div'); - this.forgotPassword = this.page.getByText('Forgot your password?'); + this.forgotPassword = this.page.getByText('I forgot my password'); this.dontHaveAccountText = this.page.getByText('Don’t have an account?'); this.createAccount = this.page.getByRole('link', { name: 'Create account' }); this.termsAndConditions = this.page.getByRole('link', { name: 'Terms and conditions' }); @@ -48,7 +48,7 @@ export class LoginPage { async typeEmail(user: string | any) { await expect(this.emailInput).toBeVisible(); const emailPlaceholder = await this.emailInput.getAttribute('placeholder'); - expect(emailPlaceholder).toEqual('Email'); + expect(emailPlaceholder).toEqual('Email address'); await this.emailInput.fill(user); } async typePassword(password: string | any) { diff --git a/test/e2e/tests/pages/signUpPage.ts b/test/e2e/tests/pages/signUpPage.ts index e10b338417..babd606ef3 100644 --- a/test/e2e/tests/pages/signUpPage.ts +++ b/test/e2e/tests/pages/signUpPage.ts @@ -25,7 +25,7 @@ export class SignUpPage { constructor(page: Page) { this.page = page; (this.createAccountTitle = this.page.getByRole('heading', { level: 1 })), - (this.emailInput = this.page.getByPlaceholder('Email', { exact: true })); + (this.emailInput = this.page.getByPlaceholder('Email address', { exact: true })); this.passwordInput = this.page.getByPlaceholder('Password', { exact: true }); this.passwordWarning = this.page.locator('[class="pt-1"] p'); this.disclaimer = this.page.locator('[class$="pr-4 dark:bg-primary/20"] p'); @@ -49,7 +49,7 @@ export class SignUpPage { const createAccountTitle = await this.createAccountTitle.textContent(); expect(createAccountTitle).toEqual('Create account'); const emailPlaceholder = await this.emailInput.getAttribute('placeholder'); - expect(emailPlaceholder).toEqual('Email'); + expect(emailPlaceholder).toEqual('Email address'); await this.emailInput.fill(email); }