import type { UserVerificationResponse } from "@/accounts/types/user"; import log from "@/base/log"; import { ensure } from "@/utils/ensure"; import { VerticallyCentered } from "@ente/shared/components/Container"; import EnteSpinner from "@ente/shared/components/EnteSpinner"; import FormPaper from "@ente/shared/components/Form/FormPaper"; import FormPaperTitle from "@ente/shared/components/Form/FormPaper/Title"; import LinkButton from "@ente/shared/components/LinkButton"; import { LoginFlowFormFooter, VerifyingPasskey, } from "@ente/shared/components/LoginComponents"; import SingleInputForm, { type SingleInputFormProps, } from "@ente/shared/components/SingleInputForm"; import { ApiError } from "@ente/shared/error"; import localForage from "@ente/shared/storage/localForage"; import { LS_KEYS, getData, 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, Stack, 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"; import { putAttributes, sendOtt, verifyOtt } from "../api/user"; import { PAGES } from "../constants/pages"; import { openPasskeyVerificationURL, passkeyVerificationRedirectURL, } from "../services/passkey"; import { unstashRedirect } from "../services/redirect"; import { configureSRP } from "../services/srp"; import type { PageProps } from "../types/page"; import type { SRPSetupAttributes } from "../types/srp"; const Page: React.FC = ({ appContext }) => { const { logout, showNavBar, setDialogBoxAttributesV2 } = appContext; const [email, setEmail] = useState(""); const [resend, setResend] = useState(0); const [passkeyVerificationData, setPasskeyVerificationData] = useState< { passkeySessionID: string; url: string } | undefined >(); const router = useRouter(); useEffect(() => { const main = async () => { const user: User = getData(LS_KEYS.USER); const keyAttributes: KeyAttributes = getData( LS_KEYS.KEY_ATTRIBUTES, ); if (!user?.email) { router.push("/"); } else if ( keyAttributes?.encryptedKey && (user.token || user.encryptedToken) ) { router.push(PAGES.CREDENTIALS); } else { setEmail(user.email); } }; main(); showNavBar(true); }, []); const onSubmit: SingleInputFormProps["callback"] = async ( ott, setFieldError, ) => { try { const referralSource = getLocalReferralSource(); const resp = await verifyOtt(email, ott, referralSource); const { keyAttributes, encryptedToken, token, id, twoFactorSessionID, passkeySessionID, } = resp.data as UserVerificationResponse; 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(passkeySessionID); setPasskeyVerificationData({ passkeySessionID, url }); openPasskeyVerificationURL({ passkeySessionID, url }); } else if (twoFactorSessionID) { await setLSUser({ email, twoFactorSessionID, isTwoFactorEnabled: true, }); setIsFirstLogin(true); 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( ensure(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); } } localForage.clear(); setIsFirstLogin(true); const redirectURL = unstashRedirect(); if (keyAttributes?.encryptedKey) { clearKeys(); router.push(redirectURL ?? PAGES.CREDENTIALS); } else { router.push(redirectURL ?? PAGES.GENERATE); } } } catch (e) { if (e instanceof ApiError) { if (e?.httpStatusCode === HttpStatusCode.Unauthorized) { setFieldError(t("INVALID_CODE")); } else if (e?.httpStatusCode === HttpStatusCode.Gone) { setFieldError(t("EXPIRED_CODE")); } } else { log.error("OTT verification failed", e); setFieldError(`${t("UNKNOWN_ERROR")} ${JSON.stringify(e)}`); } } }; const resendEmail = async () => { setResend(1); await sendOtt(email); 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, setDialogBoxAttributesV2 }} /> ); } return ( , }} values={{ email }} /> {t("CHECK_INBOX")} {resend === 0 && ( {t("RESEND_MAIL")} )} {resend === 1 && {t("SENDING")}} {resend === 2 && {t("SENT")}} {t("CHANGE_EMAIL")} ); }; export default Page;