import React, { useMemo, useState } from "react"; import { observer } from "mobx-react"; import Image from "next/image"; import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff, Sparkles } from "lucide-react"; // types import { IUser, TUserProfile, TOnboardingSteps } from "@plane/types"; // ui import { Button, Input, 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 profileSetup from "public/onboarding/profile-setup.png"; type TProfileSetupFormValues = { first_name: string; last_name: string; avatar?: string | null; password?: string; confirm_password?: string; use_case?: string; }; const defaultValues: Partial = { first_name: "", last_name: "", avatar: "", password: undefined, confirm_password: undefined, use_case: undefined, }; type Props = { user?: IUser; totalSteps: number; stepChange: (steps: Partial) => Promise; finishOnboarding: () => Promise; }; const USE_CASES = [ "Build Products", "Manage Feedbacks", "Service delivery", "Field force management", "Code Repository Integration", "Bug Tracking", "Test Case Management", "Resource allocation", ]; const fileService = new FileService(); const authService = new AuthService(); export const ProfileSetup: React.FC = observer((props) => { const { user, totalSteps, stepChange, finishOnboarding } = props; // states const [isRemoving, setIsRemoving] = useState(false); const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false); const [showPassword, setShowPassword] = useState(false); // 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 handleUserDetailUpdate = async (data: Partial) => { await updateCurrentUser(data); }; const handleUserProfileUpdate = async (data: Partial) => { await updateUserProfile(data); }; const handleSetPassword = async (password: string) => { const token = await authService.requestCSRFToken().then((data) => data?.csrf_token); await authService.setPassword(token, { password }); }; const onSubmit = async (formData: TProfileSetupFormValues) => { if (!user) return; const userDetailsPayload: Partial = { first_name: formData.first_name, last_name: formData.last_name, avatar: formData.avatar, }; const profileUpdatePayload: Partial = { use_case: formData.use_case, }; try { await Promise.all([ handleUserDetailUpdate(userDetailsPayload), handleUserProfileUpdate(profileUpdatePayload), formData.password ? handleSetPassword(formData.password) : Promise.resolve(), stepChange({ profile_complete: true }), ]).then(() => { captureEvent(USER_DETAILS, { state: "SUCCESS", element: "Onboarding", }); setToast({ type: TOAST_TYPE.SUCCESS, title: "Success", message: "Profile setup completed!", }); if (totalSteps === 1) { finishOnboarding(); } }); } catch { captureEvent(USER_DETAILS, { state: "FAILED", element: "Onboarding", }); setToast({ type: TOAST_TYPE.ERROR, title: "Error", message: "Profile setup failed. Please try again!", }); } }; 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; const isButtonDisabled = useMemo( () => isValid && (isPasswordAlreadySetup ? true : isSignUpUsingMagicCode ? !!password && isValidPassword(password, confirmPassword) : !!password ? isValidPassword(password, confirmPassword) : true) ? false : true, [isValid, isPasswordAlreadySetup, isSignUpUsingMagicCode, password, confirmPassword] ); return (

Welcome to Plane!

Let’s setup your profile, tell us a bit about yourself.

( 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 ? ( setShowPassword(false)} /> ) : ( setShowPassword(true)} /> )}
)} /> {isPasswordInputFocused && } {errors.password && {errors.password.message}}
)} {!isPasswordAlreadySetup && password && getPasswordStrength(password) >= 3 && (
value === password || "Password doesn't match", }} render={({ field: { value, onChange, ref } }) => (
{showPassword ? ( setShowPassword(false)} /> ) : ( setShowPassword(true)} /> )}
)} /> {errors.confirm_password && ( {errors.confirm_password.message} )}
)}
(
{USE_CASES.map((useCase) => (
onChange(useCase)} > {useCase}
))}
)} /> {errors.use_case && {errors.use_case.message}}
Let your team assign, mention and discuss how your work is progressing.
profile-setup
); });