import React, { useMemo, useState } from "react"; import { observer } from "mobx-react"; import Image from "next/image"; import { useTheme } from "next-themes"; import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff } from "lucide-react"; // types import { IUser, TUserProfile, TOnboardingSteps } from "@plane/types"; // ui import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui"; // components import { PasswordStrengthMeter } from "@/components/account"; import { UserImageUploadModal } from "@/components/core"; import { OnboardingHeader, SwitchOrDeleteAccountDropdown } from "@/components/onboarding"; // constants import { USER_DETAILS } from "@/constants/event-tracker"; // helpers import { getPasswordStrength } from "@/helpers/password.helper"; // hooks import { useEventTracker, useUser, useUserProfile } from "@/hooks/store"; // services import { AuthService } from "@/services/auth.service"; import { FileService } from "@/services/file.service"; // assets import ProfileSetupDark from "public/onboarding/profile-setup-dark.svg"; import ProfileSetupLight from "public/onboarding/profile-setup-light.svg"; import UserPersonalizationDark from "public/onboarding/user-personalization-dark.svg"; import UserPersonalizationLight from "public/onboarding/user-personalization-light.svg"; type TProfileSetupFormValues = { first_name: string; last_name: string; avatar?: string | null; password?: string; confirm_password?: string; role?: string; use_case?: string; }; const defaultValues: Partial = { first_name: "", last_name: "", avatar: "", password: undefined, confirm_password: undefined, role: undefined, use_case: undefined, }; type Props = { user?: IUser; totalSteps: number; stepChange: (steps: Partial) => Promise; finishOnboarding: () => Promise; }; enum EProfileSetupSteps { ALL = "ALL", USER_DETAILS = "USER_DETAILS", USER_PERSONALIZATION = "USER_PERSONALIZATION", } const USER_ROLE = ["Individual contributor", "Senior Leader", "Manager", "Executive", "Freelancer", "Student"]; const USER_DOMAIN = [ "Engineering", "Product", "Marketing", "Sales", "Operations", "Legal", "Finance", "Human Resources", "Project", "Other", ]; const fileService = new FileService(); const authService = new AuthService(); export const ProfileSetup: React.FC = observer((props) => { const { user, totalSteps, stepChange, finishOnboarding } = props; // states const [profileSetupStep, setProfileSetupStep] = useState( user?.is_password_autoset ? EProfileSetupSteps.USER_DETAILS : EProfileSetupSteps.ALL ); const [isRemoving, setIsRemoving] = useState(false); const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false); const [showPassword, setShowPassword] = useState({ password: false, retypePassword: false, }); // hooks const { resolvedTheme } = useTheme(); // store hooks const { updateCurrentUser } = useUser(); const { updateUserProfile } = useUserProfile(); const { captureEvent } = useEventTracker(); // form info const { getValues, handleSubmit, control, watch, setValue, formState: { errors, isSubmitting, isValid }, } = useForm({ defaultValues: { ...defaultValues, first_name: user?.first_name, last_name: user?.last_name, avatar: user?.avatar, }, mode: "onChange", }); const handleShowPassword = (key: keyof typeof showPassword) => setShowPassword((prev) => ({ ...prev, [key]: !prev[key] })); const handleSetPassword = async (password: string) => { const token = await authService.requestCSRFToken().then((data) => data?.csrf_token); await authService.setPassword(token, { password }); }; const handleSubmitProfileSetup = async (formData: TProfileSetupFormValues) => { const userDetailsPayload: Partial = { first_name: formData.first_name, last_name: formData.last_name, avatar: formData.avatar, }; const profileUpdatePayload: Partial = { use_case: formData.use_case, role: formData.role, }; try { await Promise.all([ updateCurrentUser(userDetailsPayload), updateUserProfile(profileUpdatePayload), totalSteps > 2 && stepChange({ profile_complete: true }), ]); captureEvent(USER_DETAILS, { state: "SUCCESS", element: "Onboarding", }); setToast({ type: TOAST_TYPE.SUCCESS, title: "Success", message: "Profile setup completed!", }); // For Invited Users, they will skip all other steps and finish onboarding. if (totalSteps <= 2) { finishOnboarding(); } } catch { captureEvent(USER_DETAILS, { state: "FAILED", element: "Onboarding", }); setToast({ type: TOAST_TYPE.ERROR, title: "Error", message: "Profile setup failed. Please try again!", }); } }; const handleSubmitUserDetail = async (formData: TProfileSetupFormValues) => { const userDetailsPayload: Partial = { first_name: formData.first_name, last_name: formData.last_name, avatar: formData.avatar, }; try { await Promise.all([ updateCurrentUser(userDetailsPayload), formData.password && handleSetPassword(formData.password), ]).then(() => setProfileSetupStep(EProfileSetupSteps.USER_PERSONALIZATION)); } catch { captureEvent(USER_DETAILS, { state: "FAILED", element: "Onboarding", }); setToast({ type: TOAST_TYPE.ERROR, title: "Error", message: "User details update failed. Please try again!", }); } }; const handleSubmitUserPersonalization = async (formData: TProfileSetupFormValues) => { const profileUpdatePayload: Partial = { use_case: formData.use_case, role: formData.role, }; try { await Promise.all([ updateUserProfile(profileUpdatePayload), totalSteps > 2 && stepChange({ profile_complete: true }), ]); captureEvent(USER_DETAILS, { state: "SUCCESS", element: "Onboarding", }); setToast({ type: TOAST_TYPE.SUCCESS, title: "Success", message: "Profile setup completed!", }); // For Invited Users, they will skip all other steps and finish onboarding. if (totalSteps <= 2) { finishOnboarding(); } } catch { captureEvent(USER_DETAILS, { state: "FAILED", element: "Onboarding", }); setToast({ type: TOAST_TYPE.ERROR, title: "Error", message: "Profile setup failed. Please try again!", }); } }; const onSubmit = async (formData: TProfileSetupFormValues) => { if (!user) return; if (profileSetupStep === EProfileSetupSteps.ALL) await handleSubmitProfileSetup(formData); if (profileSetupStep === EProfileSetupSteps.USER_DETAILS) await handleSubmitUserDetail(formData); if (profileSetupStep === EProfileSetupSteps.USER_PERSONALIZATION) await handleSubmitUserPersonalization(formData); }; const handleDelete = (url: string | null | undefined) => { if (!url) return; setIsRemoving(true); fileService.deleteUserFile(url).finally(() => { setValue("avatar", ""); setIsRemoving(false); }); }; const isPasswordAlreadySetup = !user?.is_password_autoset; const isSignUpUsingMagicCode = user?.last_login_medium === "magic-code"; const password = watch("password"); const confirmPassword = watch("confirm_password"); const isValidPassword = (password: string, confirmPassword?: string) => getPasswordStrength(password) >= 3 && password === confirmPassword; // Check for all available fields validation and if password field is available, then checks for password validation (strength + confirmation). // Also handles the condition for optional password i.e if password field is optional it only checks for above validation if it's not empty. const isButtonDisabled = useMemo( () => !isSubmitting && isValid && (isPasswordAlreadySetup ? true : isSignUpUsingMagicCode ? !!password && isValidPassword(password, confirmPassword) : !!password ? isValidPassword(password, confirmPassword) : true) ? false : true, [isSubmitting, isValid, isPasswordAlreadySetup, isSignUpUsingMagicCode, password, confirmPassword] ); const isCurrentStepUserPersonalization = profileSetupStep === EProfileSetupSteps.USER_PERSONALIZATION; return (

{isCurrentStepUserPersonalization ? `Looking good${user?.first_name && `, ${user.first_name}`}!` : "Welcome to Plane!"}

{isCurrentStepUserPersonalization ? "Let’s personalize Plane for you." : "Let’s setup your profile, tell us a bit about yourself."}

{profileSetupStep !== EProfileSetupSteps.USER_PERSONALIZATION && ( <> ( setIsImageUploadModalOpen(false)} isRemoving={isRemoving} handleDelete={() => handleDelete(getValues("avatar"))} onSuccess={(url) => { onChange(url); setIsImageUploadModalOpen(false); }} value={value && value.trim() !== "" ? value : null} /> )} />
( )} /> {errors.first_name && {errors.first_name.message}}
( )} /> {errors.last_name && {errors.last_name.message}}
{!isPasswordAlreadySetup && (
(
setIsPasswordInputFocused(true)} onBlur={() => setIsPasswordInputFocused(false)} /> {showPassword.password ? ( handleShowPassword("password")} /> ) : ( handleShowPassword("password")} /> )}
)} /> {isPasswordInputFocused && } {errors.password && {errors.password.message}}
)} {!isPasswordAlreadySetup && (
value === password || "Passwords don't match", }} render={({ field: { value, onChange, ref } }) => (
{showPassword.retypePassword ? ( handleShowPassword("retypePassword")} /> ) : ( handleShowPassword("retypePassword")} /> )}
)} /> {errors.confirm_password && ( {errors.confirm_password.message} )}
)} )} {profileSetupStep !== EProfileSetupSteps.USER_DETAILS && ( <>
(
{USER_ROLE.map((userRole) => (
onChange(userRole)} > {userRole}
))}
)} /> {errors.role && {errors.role.message}}
(
{USER_DOMAIN.map((userDomain) => (
onChange(userDomain)} > {userDomain}
))}
)} /> {errors.use_case && {errors.use_case.message}}
)}
{profileSetupStep === EProfileSetupSteps.USER_PERSONALIZATION ? ( User Personalization ) : ( Profile setup )}
); });