import crypto from "crypto"; import { GetServerSidePropsContext } from "next"; import { signOut } from "next-auth/react"; import { useRouter } from "next/router"; import { ComponentProps, RefObject, FormEvent, useEffect, useMemo, useRef, useState, BaseSyntheticEvent, } from "react"; import { useForm } from "react-hook-form"; import TimezoneSelect, { ITimezone } from "react-timezone-select"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import showToast from "@calcom/lib/notification"; import prisma from "@calcom/prisma"; import { TRPCClientErrorLike } from "@calcom/trpc/client"; import { trpc } from "@calcom/trpc/react"; import { AppRouter } from "@calcom/trpc/server/routers/_app"; import { Alert } from "@calcom/ui/Alert"; import Badge from "@calcom/ui/Badge"; import Button from "@calcom/ui/Button"; import ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent"; import { Dialog, DialogTrigger } from "@calcom/ui/Dialog"; import { Icon } from "@calcom/ui/Icon"; import { Form, PasswordField } from "@calcom/ui/form/fields"; import { Label } from "@calcom/ui/form/fields"; import { withQuery } from "@lib/QueryCell"; import { asStringOrNull, asStringOrUndefined } from "@lib/asStringOrNull"; import { ErrorCode, getSession } from "@lib/auth"; import { nameOfDay } from "@lib/core/i18n/weekday"; import { isBrandingHidden } from "@lib/isBrandingHidden"; import { inferSSRProps } from "@lib/types/inferSSRProps"; import ImageUploader from "@components/ImageUploader"; import SettingsShell from "@components/SettingsShell"; import TwoFactor from "@components/auth/TwoFactor"; import Avatar from "@components/ui/Avatar"; import InfoBadge from "@components/ui/InfoBadge"; import { UsernameAvailability } from "@components/ui/UsernameAvailability"; import ColorPicker from "@components/ui/colorpicker"; import Select from "@components/ui/form/Select"; import { UpgradeToProDialog } from "../../components/UpgradeToProDialog"; type Props = inferSSRProps; function HideBrandingInput(props: { hideBrandingRef: RefObject; user: Props["user"] }) { const { user } = props; const { t } = useLocale(); const [modalOpen, setModalOpen] = useState(false); return ( <> { if (!e.currentTarget.checked || user.plan !== "FREE") { return; } // prevent checking the input e.preventDefault(); setModalOpen(true); }} /> {t("remove_cal_branding_description")} ); } interface DeleteAccountValues { totpCode: string; } function SettingsView(props: ComponentProps & { localeProp: string }) { const { user } = props; const form = useForm(); const { t } = useLocale(); const router = useRouter(); const utils = trpc.useContext(); const onSuccessMutation = async () => { showToast(t("your_user_profile_updated_successfully"), "success"); setHasErrors(false); // dismiss any open errors await utils.invalidateQueries(["viewer.me"]); }; const onErrorMutation = (error: TRPCClientErrorLike) => { setHasErrors(true); setErrorMessage(error.message); document?.getElementsByTagName("main")[0]?.scrollTo({ top: 0, behavior: "smooth" }); }; const mutation = trpc.useMutation("viewer.updateProfile", { onSuccess: onSuccessMutation, onError: onErrorMutation, async onSettled() { await utils.invalidateQueries(["viewer.public.i18n"]); }, }); const onDeleteMeSuccessMutation = async () => { await utils.invalidateQueries(["viewer.me"]); showToast(t("Your account was deleted"), "success"); setHasDeleteErrors(false); // dismiss any open errors if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") { signOut({ callbackUrl: "/auth/logout?survey=true" }); } else { signOut({ callbackUrl: "/auth/logout" }); } }; const onDeleteMeErrorMutation = (error: TRPCClientErrorLike) => { setHasDeleteErrors(true); setDeleteErrorMessage(errorMessages[error.message]); }; const deleteMeMutation = trpc.useMutation("viewer.deleteMe", { onSuccess: onDeleteMeSuccessMutation, onError: onDeleteMeErrorMutation, async onSettled() { await utils.invalidateQueries(["viewer.me"]); }, }); const localeOptions = useMemo(() => { return (router.locales || []).map((locale) => ({ value: locale, label: new Intl.DisplayNames(props.localeProp, { type: "language" }).of(locale) || "", })); }, [props.localeProp, router.locales]); const themeOptions = [ { value: "light", label: t("light") }, { value: "dark", label: t("dark") }, ]; const timeFormatOptions = [ { value: 12, label: t("12_hour") }, { value: 24, label: t("24_hour") }, ]; const usernameRef = useRef(null!); const passwordRef = useRef(null!); const nameRef = useRef(null!); const emailRef = useRef(null!); const descriptionRef = useRef(null!); const avatarRef = useRef(null!); const hideBrandingRef = useRef(null!); const allowDynamicGroupBookingRef = useRef(null!); const [selectedTheme, setSelectedTheme] = useState(); const [selectedTimeFormat, setSelectedTimeFormat] = useState({ value: user.timeFormat || 12, label: timeFormatOptions.find((option) => option.value === user.timeFormat)?.label || 12, }); const [selectedTimeZone, setSelectedTimeZone] = useState(user.timeZone); const [selectedWeekStartDay, setSelectedWeekStartDay] = useState({ value: user.weekStart, label: nameOfDay(props.localeProp, user.weekStart === "Sunday" ? 0 : 1), }); const [selectedLanguage, setSelectedLanguage] = useState({ value: props.localeProp || "", label: localeOptions.find((option) => option.value === props.localeProp)?.label || "", }); const [imageSrc, setImageSrc] = useState(user.avatar || ""); const [hasErrors, setHasErrors] = useState(false); const [hasDeleteErrors, setHasDeleteErrors] = useState(false); const [errorMessage, setErrorMessage] = useState(""); const errorMessages: { [key: string]: string } = { [ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"), [ErrorCode.IncorrectPassword]: `${t("incorrect_password")} ${t("please_try_again")}`, [ErrorCode.UserNotFound]: t("no_account_exists"), [ErrorCode.IncorrectTwoFactorCode]: `${t("incorrect_2fa_code")} ${t("please_try_again")}`, [ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`, [ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"), }; const [deleteErrorMessage, setDeleteErrorMessage] = useState(""); const [brandColor, setBrandColor] = useState(user.brandColor); const [darkBrandColor, setDarkBrandColor] = useState(user.darkBrandColor); useEffect(() => { if (!user.theme) return; const userTheme = themeOptions.find((theme) => theme.value === user.theme); if (!userTheme) return; setSelectedTheme(userTheme); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const onConfirmButton = (e: FormEvent) => { e.preventDefault(); const totpCode = form.getValues("totpCode"); const password = passwordRef.current.value; deleteMeMutation.mutate({ password, totpCode }); }; const onConfirm = ({ totpCode }: DeleteAccountValues, e: BaseSyntheticEvent | undefined) => { e?.preventDefault(); const password = passwordRef.current.value; deleteMeMutation.mutate({ password, totpCode }); }; async function updateProfileHandler(event: FormEvent) { event.preventDefault(); const enteredUsername = usernameRef.current.value.toLowerCase(); const enteredName = nameRef.current.value; const enteredEmail = emailRef.current.value; const enteredDescription = descriptionRef.current.value; const enteredAvatar = avatarRef.current.value; const enteredBrandColor = brandColor; const enteredDarkBrandColor = darkBrandColor; const enteredTimeZone = typeof selectedTimeZone === "string" ? selectedTimeZone : selectedTimeZone.value; const enteredWeekStartDay = selectedWeekStartDay.value; const enteredHideBranding = hideBrandingRef.current.checked; const enteredAllowDynamicGroupBooking = allowDynamicGroupBookingRef.current.checked; const enteredLanguage = selectedLanguage.value; const enteredTimeFormat = selectedTimeFormat.value; // Write time format to localStorage if available // Embed isn't applicable to profile pages. So ignore the rule // eslint-disable-next-line @calcom/eslint/avoid-web-storage window.localStorage.setItem("timeOption.is24hClock", selectedTimeFormat.value === 12 ? "false" : "true"); // TODO: Add validation mutation.mutate({ username: enteredUsername, name: enteredName, email: enteredEmail, bio: enteredDescription, avatar: enteredAvatar, timeZone: enteredTimeZone, weekStart: asStringOrUndefined(enteredWeekStartDay), hideBranding: enteredHideBranding, allowDynamicBooking: enteredAllowDynamicGroupBooking, theme: asStringOrNull(selectedTheme?.value), brandColor: enteredBrandColor, darkBrandColor: enteredDarkBrandColor, locale: enteredLanguage, timeFormat: enteredTimeFormat, }); } const [currentUsername, setCurrentUsername] = useState(user.username || undefined); const [inputUsernameValue, setInputUsernameValue] = useState(currentUsername); return ( <>
{hasErrors && }

{t("change_email_tip")}