import { AccountsPageContents, AccountsPageFooter, AccountsPageTitle, } from "@/accounts/components/layouts/centered-paper"; import { VerifyingPasskey } from "@/accounts/components/LoginComponents"; import { SecondFactorChoice } from "@/accounts/components/SecondFactorChoice"; import { useSecondFactorChoiceIfNeeded } from "@/accounts/components/utils/second-factor-choice"; import { PAGES } from "@/accounts/constants/pages"; import { openPasskeyVerificationURL, passkeyVerificationRedirectURL, } from "@/accounts/services/passkey"; import { stashedRedirect, unstashRedirect } from "@/accounts/services/redirect"; import { configureSRP } from "@/accounts/services/srp"; import type { SRPAttributes, SRPSetupAttributes, } from "@/accounts/services/srp-remote"; import { getSRPAttributes } from "@/accounts/services/srp-remote"; import { putAttributes, sendOTT, verifyEmail } from "@/accounts/services/user"; import { LinkButton } from "@/base/components/LinkButton"; import { LoadingIndicator } from "@/base/components/loaders"; import { useBaseContext } from "@/base/context"; import log from "@/base/log"; import SingleInputForm, { type SingleInputFormProps, } from "@ente/shared/components/SingleInputForm"; import { ApiError } from "@ente/shared/error"; import localForage from "@ente/shared/storage/localForage"; import { getData, LS_KEYS, setData, setLSUser, } from "@ente/shared/storage/localStorage"; import { getLocalReferralSource, setIsFirstLogin, } from "@ente/shared/storage/localStorage/helpers"; import { clearKeys } from "@ente/shared/storage/sessionStorage"; import type { KeyAttributes, User } from "@ente/shared/user/types"; import { Box, Typography } from "@mui/material"; import { HttpStatusCode } from "axios"; import { t } from "i18next"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { Trans } from "react-i18next"; const Page: React.FC = () => { const { logout, showMiniDialog } = useBaseContext(); const [email, setEmail] = useState(""); const [resend, setResend] = useState(0); const [passkeyVerificationData, setPasskeyVerificationData] = useState< { passkeySessionID: string; url: string } | undefined >(); const { secondFactorChoiceProps, userVerificationResultAfterResolvingSecondFactorChoice, } = useSecondFactorChoiceIfNeeded(); const router = useRouter(); useEffect(() => { const main = async () => { const user: User = getData(LS_KEYS.USER); const redirect = await redirectionIfNeeded(user); if (redirect) { void router.push(redirect); } else { setEmail(user.email); } }; void main(); }, [router]); const onSubmit: SingleInputFormProps["callback"] = async ( ott, setFieldError, ) => { try { const referralSource = getLocalReferralSource()?.trim(); const cleanedReferral = referralSource ? `web:${referralSource}` : undefined; const { keyAttributes, encryptedToken, token, id, twoFactorSessionID, passkeySessionID, accountsUrl, } = await userVerificationResultAfterResolvingSecondFactorChoice( await verifyEmail(email, ott, cleanedReferral), ); if (passkeySessionID) { const user = getData(LS_KEYS.USER); await setLSUser({ ...user, passkeySessionID, isTwoFactorEnabled: true, isTwoFactorPasskeysEnabled: true, }); // TODO: This is not the first login though if they already have // 2FA. Does this flag mean first login on this device? // // Update: This flag causes the interactive encryption key to be // generated, so it has a functional impact we need. setIsFirstLogin(true); const url = passkeyVerificationRedirectURL( accountsUrl, passkeySessionID, ); setPasskeyVerificationData({ passkeySessionID, url }); openPasskeyVerificationURL({ passkeySessionID, url }); } else if (twoFactorSessionID) { await setLSUser({ email, twoFactorSessionID, isTwoFactorEnabled: true, }); setIsFirstLogin(true); void router.push(PAGES.TWO_FACTOR_VERIFY); } else { await setLSUser({ email, token, encryptedToken, id, isTwoFactorEnabled: false, }); if (keyAttributes) { setData(LS_KEYS.KEY_ATTRIBUTES, keyAttributes); setData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES, keyAttributes); } else { if (getData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES)) { await putAttributes( token!, getData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES), ); } if (getData(LS_KEYS.SRP_SETUP_ATTRIBUTES)) { const srpSetupAttributes: SRPSetupAttributes = getData( LS_KEYS.SRP_SETUP_ATTRIBUTES, ); await configureSRP(srpSetupAttributes); } } await localForage.clear(); setIsFirstLogin(true); const redirectURL = unstashRedirect(); if (keyAttributes?.encryptedKey) { clearKeys(); void router.push(redirectURL ?? PAGES.CREDENTIALS); } else { void router.push(redirectURL ?? PAGES.GENERATE); } } } catch (e) { if (e instanceof ApiError) { // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison if (e?.httpStatusCode === HttpStatusCode.Unauthorized) { setFieldError(t("invalid_code_error")); // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison } else if (e?.httpStatusCode === HttpStatusCode.Gone) { setFieldError(t("expired_code_error")); } } else { log.error("OTT verification failed", e); setFieldError(t("generic_error_retry")); } } }; const resendEmail = async () => { setResend(1); await sendOTT(email, undefined); setResend(2); setTimeout(() => setResend(0), 3000); }; if (!email) { return ; } if (passkeyVerificationData) { // We only need to handle this scenario when running in the desktop app // because the web app will navigate to Passkey verification URL. // However, still we add an additional `globalThis.electron` check to // show a spinner. This prevents the VerifyingPasskey component from // being disorientingly shown for a fraction of a second as the redirect // happens on the web app. // // See: [Note: Passkey verification in the desktop app] if (!globalThis.electron) { return ; } return ( openPasskeyVerificationURL(passkeyVerificationData) } {...{ logout, showMiniDialog }} /> ); } return ( ), }} values={{ email }} /> {t("check_inbox_hint")} {resend === 0 && ( {t("resend_code")} )} {resend === 1 && {t("status_sending")}} {resend === 2 && {t("status_sent")}} {t("change_email")} ); }; export default Page; /** * A function called during page load to see if a redirection is required * * @returns The slug to redirect to, if needed. */ const redirectionIfNeeded = async (user: User | undefined) => { const email = user?.email; if (!email) { return "/"; } const keyAttributes: KeyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES); if (keyAttributes?.encryptedKey && (user.token || user.encryptedToken)) { return PAGES.CREDENTIALS; } // If we're coming here during the recover flow, do not redirect. if (stashedRedirect() == PAGES.RECOVER) return undefined; // The user might have email verification disabled, but after previously // entering their email on the login screen, they might've closed the tab // before proceeding (or opened a us in a new tab at this point). // // In such cases, we'll end up here with an email present. // // To distinguish this scenario from the normal email verification flow, we // can check to see the SRP attributes (the login page would've fetched and // saved them). If they are present and indicate that email verification is // not required, redirect to the password verification page. const srpAttributes: SRPAttributes = getData(LS_KEYS.SRP_ATTRIBUTES); if (srpAttributes && !srpAttributes.isEmailMFAEnabled) { // Fetch the latest SRP attributes instead of relying on the potentially // stale stored values. This is an infrequent scenario path, so extra // API calls are fine. const latestSRPAttributes = await getSRPAttributes(email); if (latestSRPAttributes && !latestSRPAttributes.isEmailMFAEnabled) { return PAGES.CREDENTIALS; } } return undefined; };