Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/app/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@
"valentines": "Valentinstag"
},
"auth": {
"accountSetupPending": {
"title": "Schließen Sie die Einrichtung Ihres Kontos ab",
"message": "Sie haben mit dieser E-Mail bereits einen Tarif gekauft. Verwenden Sie den Link, den wir Ihnen gesendet haben, um Ihr Passwort festzulegen.",
"resend": "E-Mail erneut senden",
"emailSent": "E-Mail gesendet. Sehen Sie in Ihrem Posteingang nach.",
"resendFailed": "Wir konnten die E-Mail nicht senden. Bitte versuchen Sie es erneut."
},
"emailFloatingLabel": "E-Mail-Adresse",
"email": "E-Mail",
"passwordFloatingLabel": "Passwort",
Expand Down
7 changes: 7 additions & 0 deletions src/app/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@
"environment": "World Environment Day"
},
"auth": {
"accountSetupPending": {
"title": "Finish setting up your account",
"message": "You already bought a plan with this email. Use the link we sent you to create your password.",
"resend": "Resend the email",
"emailSent": "Email sent. Check your inbox.",
"resendFailed": "We couldn't send the email. Please try again."
},
"emailFloatingLabel": "Email address",
"email": "Email address",
"passwordFloatingLabel": "Password",
Expand Down
7 changes: 7 additions & 0 deletions src/app/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@
"environment": "Día Mundial del Medio Ambiente"
},
"auth": {
"accountSetupPending": {
"title": "Termina de configurar tu cuenta",
"message": "Ya has comprado un plan con este email. Usa el enlace que te enviamos para crear tu contraseña.",
"resend": "Reenviar el correo",
"emailSent": "Correo enviado. Revisa tu bandeja de entrada.",
"resendFailed": "No hemos podido enviar el correo. Inténtalo de nuevo."
},
"emailFloatingLabel": "Correo electrónico",
"email": "Correo electrónico",
"passwordFloatingLabel": "Contraseña",
Expand Down
7 changes: 7 additions & 0 deletions src/app/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@
"environment": "Journée Mondiale de l'Environnement"
},
"auth": {
"accountSetupPending": {
"title": "Terminez la configuration de votre compte",
"message": "Vous avez déjà acheté un forfait avec cet e-mail. Utilisez le lien que nous vous avons envoyé pour créer votre mot de passe.",
"resend": "Renvoyer l'e-mail",
"emailSent": "E-mail envoyé. Consultez votre boîte de réception.",
"resendFailed": "Nous n'avons pas pu envoyer l'e-mail. Veuillez réessayer."
},
"emailFloatingLabel": "Adresse e-mail",
"email": "Email",
"passwordFloatingLabel": "Mot de passe",
Expand Down
7 changes: 7 additions & 0 deletions src/app/i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@
"environment": "Giornata Mondiale dell'Ambiente"
},
"auth": {
"accountSetupPending": {
"title": "Completa la configurazione del tuo account",
"message": "Hai già acquistato un piano con questa email. Usa il link che ti abbiamo inviato per creare la tua password.",
"resend": "Invia di nuovo l'email",
"emailSent": "Email inviata. Controlla la tua casella di posta.",
"resendFailed": "Non è stato possibile inviare l'email. Riprova."
},
"emailFloatingLabel": "Indirizzo email",
"email": "Email",
"passwordFloatingLabel": "Password",
Expand Down
7 changes: 7 additions & 0 deletions src/app/i18n/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@
"environment": "Dia Mundial do Meio Ambiente"
},
"auth": {
"accountSetupPending": {
"title": "Conclua a configuração da sua conta",
"message": "Você já comprou um plano com este e-mail. Use o link que enviamos para criar sua senha.",
"resend": "Reenviar o e-mail",
"emailSent": "E-mail enviado. Verifique sua caixa de entrada.",
"resendFailed": "Não foi possível enviar o e-mail. Tente novamente."
},
"emailFloatingLabel": "Endereço de e-mail",
"email": "Endereço de e-mail",
"passwordFloatingLabel": "Senha",
Expand Down
7 changes: 7 additions & 0 deletions src/app/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@
"environment": "World Environment Day"
},
"auth": {
"accountSetupPending": {
"title": "Завершите настройку аккаунта",
"message": "Вы уже купили тариф с этим адресом. Перейдите по ссылке из нашего письма, чтобы задать пароль.",
"resend": "Отправить письмо ещё раз",
"emailSent": "Письмо отправлено. Проверьте почту.",
"resendFailed": "Не удалось отправить письмо. Попробуйте ещё раз."
},
"emailFloatingLabel": "Электронная почта",
"email": "Email",
"passwordFloatingLabel": "Пароль",
Expand Down
7 changes: 7 additions & 0 deletions src/app/i18n/locales/tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@
"environment": "世界環境日"
},
"auth": {
"accountSetupPending": {
"title": "完成帳戶設定",
"message": "您已使用此電子郵件購買方案。請使用我們寄給您的連結設定密碼。",
"resend": "重新寄送電子郵件",
"emailSent": "電子郵件已寄出,請查看收件匣。",
"resendFailed": "無法寄送電子郵件,請再試一次。"
},
"emailFloatingLabel": "電子郵件",
"email": "電子郵件",
"passwordFloatingLabel": "密碼",
Expand Down
7 changes: 7 additions & 0 deletions src/app/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@
"environment": "世界环境日"
},
"auth": {
"accountSetupPending": {
"title": "完成账户设置",
"message": "您已使用此邮箱购买了套餐。请使用我们发送给您的链接设置密码。",
"resend": "重新发送邮件",
"emailSent": "邮件已发送,请查收收件箱。",
"resendFailed": "无法发送邮件,请重试。"
},
"emailFloatingLabel": "电子邮件",
"email": "电子邮件",
"passwordFloatingLabel": "密码",
Expand Down
31 changes: 31 additions & 0 deletions src/services/auth.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import envService from 'services/env.service';
import localStorageService from 'services/local-storage.service';
import navigationService from 'services/navigation.service';
import { BackupData } from 'utils/backupKeyUtils';
import { AxiosResponseError } from '@internxt/sdk/dist/shared/types/errors';
import { isAccountSetupPending } from 'services/account-setup.service';
import { beforeAll, beforeEach, describe, expect, it, test, vi } from 'vitest';
import * as authService from './auth.service';
import { PasswordMismatchError } from './errors/auth.errors';
Expand Down Expand Up @@ -946,6 +948,35 @@ describe('Security and validation', () => {
});
});

describe('Pending account setup on login', () => {
const mockSecurityDetailsFailure = (error: unknown) => {
vi.spyOn(SdkFactory, 'getNewApiInstance').mockReturnValue({
createAuthClient: vi.fn().mockReturnValue({ securityDetails: vi.fn().mockRejectedValue(error) }),
} as unknown as ReturnType<typeof SdkFactory.getNewApiInstance>);
};

test('When the email belongs to a paid account whose setup is not finished, then the error still says the setup is pending', async () => {
const pendingSetupError = new AxiosResponseError('Forbidden', '', {
status: 403,
data: { message: 'Account setup pending', code: 'AccountSetupPending' },
headers: {},
} as never);
mockSecurityDetailsFailure(pendingSetupError);

const loginAttempt = authService.getSecurityDetails('pending@example.com');

await expect(loginAttempt).rejects.toSatisfy(isAccountSetupPending);
});

test('When the login fails for any other reason, then the usual error is thrown', async () => {
mockSecurityDetailsFailure(new Error('Wrong login credentials'));

const loginAttempt = authService.getSecurityDetails('user@example.com');

await expect(loginAttempt).rejects.toThrow('Wrong login credentials');
});
});

describe('readReferalCookie', () => {
it('reads referral code from cookies', () => {
const originalCookie = document.cookie;
Expand Down
2 changes: 2 additions & 0 deletions src/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { planThunks } from 'app/store/slices/plan';
import { initializeUserThunk, userThunks } from 'app/store/slices/user';
import { workspaceThunks } from 'app/store/slices/workspaces/workspacesStore';
import { generateMnemonic, validateMnemonic } from 'bip39';
import { isAccountSetupPending } from 'services/account-setup.service';
import errorService from 'services/error.service';
import localStorageService from 'services/local-storage.service';
import { ATTRIBUTION_LOCAL_STORAGE_ITEMS, CHECKOUT_LOCAL_STORAGE_ITEMS } from 'services/storage-keys';
Expand Down Expand Up @@ -156,6 +157,7 @@ export function cancelAccount(): Promise<void> {
export const getSecurityDetails = async (email: string, turnstileToken?: string): Promise<SecurityDetails> => {
const authClient = SdkFactory.getNewApiInstance().createAuthClient({ turnstileToken });
return authClient.securityDetails(email).catch((error) => {
if (isAccountSetupPending(error)) throw error;
throw errorService.castError(error);
});
};
Expand Down
65 changes: 65 additions & 0 deletions src/views/Login/components/AccountSetupPendingNotice.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { fireEvent, render, screen } from '@testing-library/react';
import { SdkFactory } from 'app/core/factory/sdk';
import { AccountSetupPendingNotice } from './AccountSetupPendingNotice';

vi.mock('app/i18n/provider/TranslationProvider', () => ({
useTranslationContext: () => ({ translate: (key: string) => key }),
}));

vi.mock('app/core/factory/sdk', () => ({
SdkFactory: { getNewApiInstance: vi.fn() },
}));

vi.mock('services/error.service', () => ({
default: { reportError: vi.fn() },
}));

const PENDING_EMAIL = 'Pending@Example.com';

const mockResendEndpoint = (resendAccountSetupEmail: ReturnType<typeof vi.fn>) => {
vi.mocked(SdkFactory.getNewApiInstance).mockReturnValue({
createAuthClient: () => ({ resendAccountSetupEmail }),
} as unknown as ReturnType<typeof SdkFactory.getNewApiInstance>);
};

describe('Pending account setup notice', () => {
beforeEach(() => {
globalThis.grecaptcha = {
ready: (callback: () => void) => callback(),
execute: async () => 'captcha-token',
} as unknown as typeof globalThis.grecaptcha;
mockResendEndpoint(vi.fn().mockResolvedValue(undefined));
});

test('When the notice is shown, then the user is asked to finish the setup and can resend the email', () => {
render(<AccountSetupPendingNotice email={PENDING_EMAIL} />);

expect(screen.getByText('auth.accountSetupPending.title')).toBeInTheDocument();
expect(screen.getByText('auth.accountSetupPending.message')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'auth.accountSetupPending.resend' })).toBeInTheDocument();
});

test('When the user resends the email, then it is sent to their address and a confirmation replaces the button', async () => {
const resendAccountSetupEmail = vi.fn().mockResolvedValue(undefined);
mockResendEndpoint(resendAccountSetupEmail);
render(<AccountSetupPendingNotice email={PENDING_EMAIL} />);

fireEvent.click(screen.getByRole('button', { name: 'auth.accountSetupPending.resend' }));

expect(await screen.findByRole('status')).toHaveTextContent('auth.accountSetupPending.emailSent');
expect(resendAccountSetupEmail).toHaveBeenCalledWith('pending@example.com');
expect(screen.queryByRole('button', { name: 'auth.accountSetupPending.resend' })).not.toBeInTheDocument();
});

test('When resending the email fails, then an error is shown and the user can try again', async () => {
mockResendEndpoint(vi.fn().mockRejectedValue(new Error('Network error')));
render(<AccountSetupPendingNotice email={PENDING_EMAIL} />);

fireEvent.click(screen.getByRole('button', { name: 'auth.accountSetupPending.resend' }));

expect(await screen.findByRole('alert')).toHaveTextContent('auth.accountSetupPending.resendFailed');
expect(screen.getByRole('button', { name: 'auth.accountSetupPending.resend' })).toBeInTheDocument();
expect(screen.queryByRole('status')).not.toBeInTheDocument();
});
});
60 changes: 60 additions & 0 deletions src/views/Login/components/AccountSetupPendingNotice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useState } from 'react';
import { Button } from '@internxt/ui';
import { CheckCircle, WarningCircle } from '@phosphor-icons/react';

import { useTranslationContext } from 'app/i18n/provider/TranslationProvider';
import { resendAccountSetupEmail } from 'services/account-setup.service';
import errorService from 'services/error.service';

type ResendStatus = 'idle' | 'sending' | 'sent' | 'failed';

interface AccountSetupPendingNoticeProps {
email: string;
}

export const AccountSetupPendingNotice = ({ email }: AccountSetupPendingNoticeProps): JSX.Element => {
const { translate } = useTranslationContext();
const [resendStatus, setResendStatus] = useState<ResendStatus>('idle');

const resendEmail = async () => {
setResendStatus('sending');
try {
await resendAccountSetupEmail(email);
setResendStatus('sent');
} catch (error) {
errorService.reportError(error);
setResendStatus('failed');
}
};

return (
<div className="flex flex-col space-y-3 pt-1">
<div className="flex flex-row items-start">
<div className="flex h-5 flex-row items-center">
<WarningCircle weight="fill" className="mr-1 h-4 text-primary" />
</div>
<div className="flex flex-col text-sm">
<span className="font-medium text-gray-100">{translate('auth.accountSetupPending.title')}</span>
<span className="text-gray-60">{translate('auth.accountSetupPending.message')}</span>
</div>
</div>

{resendStatus === 'sent' ? (
<div role="status" className="flex flex-row items-center text-sm text-green">
<CheckCircle weight="fill" className="mr-1 h-4" />
<span>{translate('auth.accountSetupPending.emailSent')}</span>
</div>
) : (
<Button type="button" variant="secondary" loading={resendStatus === 'sending'} onClick={resendEmail}>
{translate('auth.accountSetupPending.resend')}
</Button>
)}

{resendStatus === 'failed' && (
<span role="alert" className="text-sm text-red">
{translate('auth.accountSetupPending.resendFailed')}
</span>
)}
</div>
);
};
Loading
Loading