1 import type { ReactNode } from 'react';
2 import { useEffect, useRef, useState } from 'react';
4 import { c } from 'ttag';
6 import { Button, CircleLoader } from '@proton/atoms';
7 import InputFieldTwo from '@proton/components/components/v2/field/InputField';
8 import PasswordInputTwo from '@proton/components/components/v2/input/PasswordInput';
9 import useFormErrors from '@proton/components/components/v2/useFormErrors';
10 import TotpInputs from '@proton/components/containers/account/totp/TotpInputs';
11 import { startUnAuthFlow } from '@proton/components/containers/api/unAuthenticatedApi';
12 import useApi from '@proton/components/hooks/useApi';
13 import useConfig from '@proton/components/hooks/useConfig';
14 import useErrorHandler from '@proton/components/hooks/useErrorHandler';
15 import useNotifications from '@proton/components/hooks/useNotifications';
16 import { useLoading } from '@proton/hooks';
17 import { getApiErrorMessage } from '@proton/shared/lib/api/helpers/apiErrorHelper';
18 import { API_CUSTOM_ERROR_CODES } from '@proton/shared/lib/errors';
19 import { requiredValidator } from '@proton/shared/lib/helpers/formValidators';
20 import { KeyTransparencyActivation } from '@proton/shared/lib/interfaces';
21 import noop from '@proton/utils/noop';
23 import type { OnLoginCallback } from '../app/interface';
24 import Challenge from '../challenge/Challenge';
25 import ChallengeError from '../challenge/ChallengeError';
26 import type { ChallengeRef, ChallengeResult } from '../challenge/interface';
27 import AbuseModal from './AbuseModal';
28 import type { AuthActionResponse, AuthCacheResult } from './interface';
29 import { AuthStep, AuthType } from './interface';
30 import { handleLogin, handleNextLogin, handleTotp, handleUnlock } from './loginActions';
36 onSubmit: (totp: string) => Promise<void>;
37 cancelButton?: ReactNode;
39 const [loading, withLoading] = useLoading();
40 const [keyPassword, setKeyPassword] = useState('');
42 const { validator, onFormSubmit } = useFormErrors();
47 onSubmit={(event) => {
48 event.preventDefault();
49 if (!onFormSubmit()) {
52 withLoading(onSubmit(keyPassword)).catch(noop);
60 label={c('Label').t`Second password`}
61 error={validator([requiredValidator(keyPassword)])}
62 disableChange={loading}
65 onValue={setKeyPassword}
67 <div className="flex justify-space-between">
74 data-cy-login="submit mailbox password"
76 {c('Action').t`Submit`}
88 onSubmit: (totp: string) => Promise<void>;
89 cancelButton?: ReactNode;
91 const [loading, withLoading] = useLoading();
93 const [code, setCode] = useState('');
94 const [type, setType] = useState<'totp' | 'recovery-code'>('totp');
95 const hasBeenAutoSubmitted = useRef(false);
97 const { validator, onFormSubmit, reset } = useFormErrors();
99 const safeCode = code.replaceAll(/\s+/g, '');
100 const requiredError = requiredValidator(safeCode);
103 if (type !== 'totp' || loading || requiredError || hasBeenAutoSubmitted.current) {
106 // Auto-submit the form once the user has entered the TOTP
107 if (safeCode.length === 6) {
108 // Do it just one time
109 hasBeenAutoSubmitted.current = true;
110 withLoading(onSubmit(safeCode)).catch(noop);
117 onSubmit={(event) => {
118 event.preventDefault();
119 if (!onFormSubmit()) {
122 withLoading(onSubmit(safeCode)).catch(noop);
129 error={validator([requiredError])}
134 <div className="flex justify-space-between">
135 <Button size="large" fullWidth color="norm" type="submit" loading={loading}>
136 {c('Action').t`Submit`}
150 setType(type === 'totp' ? 'recovery-code' : 'totp');
153 {type === 'totp' ? c('Action').t`Use recovery code` : c('Action').t`Use authentication code`}
167 onSubmit: (username: string, password: string, payload: ChallengeResult) => Promise<void>;
168 hasChallenge?: boolean;
169 needHelp?: ReactNode;
172 const [loading, withLoading] = useLoading();
173 const [username, setUsername] = useState('');
174 const [password, setPassword] = useState('');
175 const challengeRefLogin = useRef<ChallengeRef>();
176 const usernameRef = useRef<HTMLInputElement>(null);
177 const [challengeLoading, setChallengeLoading] = useState(hasChallenge);
178 const [challengeError, setChallengeError] = useState(false);
180 const { validator, onFormSubmit } = useFormErrors();
183 if (challengeLoading) {
186 // Special focus management for challenge
187 usernameRef.current?.focus();
188 }, [challengeLoading]);
190 if (challengeError) {
191 return <ChallengeError />;
196 {challengeLoading && (
197 <div className="text-center">
198 <CircleLoader className="color-primary" size="large" />
203 className={challengeLoading ? 'hidden' : undefined}
204 onSubmit={(event) => {
205 event.preventDefault();
206 if (!onFormSubmit()) {
209 const run = async () => {
210 const payload = await challengeRefLogin.current?.getChallenge().catch(noop);
211 return onSubmit(username, password, payload);
213 withLoading(run()).catch(noop);
221 challengeRef={challengeRefLogin}
225 setChallengeLoading(false);
228 setChallengeLoading(false);
229 setChallengeError(true);
236 label={c('Label').t`Email or username`}
237 error={validator([requiredValidator(username)])}
238 autoComplete="username"
240 onValue={setUsername}
246 label={c('Label').t`Password`}
247 error={validator([requiredValidator(password)])}
248 as={PasswordInputTwo}
249 autoComplete="current-password"
251 onValue={setPassword}
254 <div className="flex justify-space-between mt-4">
256 <Button color="norm" size="large" type="submit" fullWidth loading={loading} data-cy-login="submit">
257 {c('Action').t`Sign in`}
267 onLogin: OnLoginCallback;
268 needHelp?: ReactNode;
270 hasChallenge?: boolean;
271 ignoreUnlock?: boolean;
274 const MinimalLoginContainer = ({ onLogin, hasChallenge = false, ignoreUnlock = false, needHelp, footer }: Props) => {
275 const { APP_NAME } = useConfig();
276 const { createNotification } = useNotifications();
277 const [abuseModal, setAbuseModal] = useState<{ apiErrorMessage?: string } | undefined>(undefined);
279 const normalApi = useApi();
280 const silentApi = <T,>(config: any) => normalApi<T>({ ...config, silence: true });
281 const errorHandler = useErrorHandler();
283 const cacheRef = useRef<AuthCacheResult | undefined>(undefined);
284 const [step, setStep] = useState(AuthStep.LOGIN);
286 const handleResult = async (result: AuthActionResponse) => {
287 if (result.to === AuthStep.DONE) {
288 await onLogin(result.session);
291 cacheRef.current = result.cache;
295 const handleCancel = () => {
296 cacheRef.current = undefined;
297 setStep(AuthStep.LOGIN);
300 const handleError = (e: any) => {
301 if (e.data?.Code === API_CUSTOM_ERROR_CODES.AUTH_ACCOUNT_DISABLED) {
302 setAbuseModal({ apiErrorMessage: getApiErrorMessage(e) });
305 if (e.name === 'TOTPError' || e.name === 'PasswordError') {
306 createNotification({ type: 'error', text: e.message });
310 step === AuthStep.LOGIN ||
311 (step === AuthStep.UNLOCK && e.name !== 'PasswordError') ||
312 (step === AuthStep.TWO_FA && e.name !== 'TOTPError')
319 const cancelButton = (
320 <Button size="large" shape="ghost" type="button" fullWidth onClick={handleCancel} className="mt-2">
321 {c('Action').t`Cancel`}
325 const cache = cacheRef.current;
330 message={abuseModal?.apiErrorMessage}
332 onClose={() => setAbuseModal(undefined)}
334 {step === AuthStep.LOGIN && (
338 hasChallenge={hasChallenge}
339 onSubmit={async (username, password, payload) => {
341 await startUnAuthFlow();
342 const loginResult = await handleLogin({
349 const result = await handleNextLogin({
350 authType: AuthType.SRP,
351 authResponse: loginResult.authResult.result,
352 authVersion: loginResult.authResult.authVersion,
355 productParam: APP_NAME,
359 verifyOutboundPublicKeys: null,
363 ktActivation: KeyTransparencyActivation.DISABLED,
365 return await handleResult(result);
372 {step === AuthStep.TWO_FA && cache && (
374 cancelButton={cancelButton}
375 onSubmit={(totp) => {
385 {step === AuthStep.UNLOCK && cache && (
387 cancelButton={cancelButton}
388 onSubmit={(keyPassword) => {
389 return handleUnlock({
391 clearKeyPassword: keyPassword,
392 isOnePasswordMode: false,
403 export default MinimalLoginContainer;