import { sessionExpiredDialogAttributes } from "@/accounts/components/utils/dialog"; import { checkPasskeyVerificationStatus, passkeySessionExpiredErrorMessage, saveCredentialsAndNavigateTo, } from "@/accounts/services/passkey"; import { LinkButton } from "@/base/components/LinkButton"; import type { MiniDialogAttributes } from "@/base/components/MiniDialog"; import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton"; import { genericErrorDialogAttributes } from "@/base/components/utils/dialog"; import log from "@/base/log"; import { customAPIHost } from "@/base/origins"; import { CircularProgress, Stack, Typography, styled } from "@mui/material"; import { t } from "i18next"; import { useRouter } from "next/router"; import React, { useEffect, useState } from "react"; import { AccountsPageContents, AccountsPageFooter, } from "./layouts/centered-paper"; export const PasswordHeader: React.FC = ({ children, }) => { return ( {t("password")} {children} ); }; const PasskeyHeader: React.FC = ({ children }) => { return ( {t("passkey")} {children} ); }; const Header_ = styled("div")` margin-block-end: 24px; display: flex; flex-direction: column; gap: 8px; `; export const AccountsPageFooterWithHost: React.FC = ({ children, }) => { const [host, setHost] = useState(); useEffect(() => void customAPIHost().then(setHost), []); return ( {children} {host && ( {host} )} ); }; interface VerifyingPasskeyProps { /** ID of the current passkey verification session. */ passkeySessionID: string; /** The email of the user whose passkey we're verifying. */ email: string | undefined; /** Called when the user wants to redirect again. */ onRetry: () => void; /** Perform the (possibly app specific) logout sequence. */ logout: () => void; showMiniDialog: (attrs: MiniDialogAttributes) => void; } export const VerifyingPasskey: React.FC = ({ passkeySessionID, email, onRetry, logout, showMiniDialog, }) => { type VerificationStatus = "waiting" | "checking" | "pending"; const [verificationStatus, setVerificationStatus] = useState("waiting"); const router = useRouter(); const handleRetry = () => { setVerificationStatus("waiting"); onRetry(); }; const handleCheckStatus = async () => { setVerificationStatus("checking"); try { const response = await checkPasskeyVerificationStatus(passkeySessionID); if (!response) setVerificationStatus("pending"); else void router.push(await saveCredentialsAndNavigateTo(response)); } catch (e) { log.error("Passkey verification status check failed", e); showMiniDialog( e instanceof Error && e.message == passkeySessionExpiredErrorMessage ? sessionExpiredDialogAttributes(logout) : genericErrorDialogAttributes(), ); setVerificationStatus("waiting"); } }; const handleRecover = () => { void router.push("/passkeys/recover"); }; return ( {email ?? ""} {verificationStatus == "checking" ? ( ) : ( {verificationStatus == "waiting" ? t("waiting_for_verification") : t("verification_still_pending")} )} {t("try_again")} {t("check_status")} {t("recover_account")} {t("change_email")} ); }; const VerifyingPasskeyMiddle = styled("div")` display: flex; flex-direction: column; padding-block: 1rem; gap: 4rem; `; const VerifyingPasskeyStatus = styled("div")` text-align: center; /* Size of the CircularProgress (+ some margin) so that there is no layout shift when it is shown */ min-height: 2em; `; const ButtonStack = styled("div")` display: flex; flex-direction: column; gap: 1rem; `;