"use client"; import React, { useMemo, useState } from "react"; import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff } from "lucide-react"; import { AUTH_TRACKER_EVENTS, E_PASSWORD_STRENGTH, ONBOARDING_TRACKER_ELEMENTS, USER_TRACKER_EVENTS, } from "@plane/constants"; // types import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/propel/button"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import type { IUser, TUserProfile, TOnboardingSteps } from "@plane/types"; // ui import { Input, PasswordStrengthIndicator, Spinner } from "@plane/ui"; // components import { getFileURL, getPasswordStrength } from "@plane/utils"; import { UserImageUploadModal } from "@/components/core/modals/user-image-upload-modal"; // constants // helpers // hooks import { captureError, captureSuccess, captureView } from "@/helpers/event-tracker.helper"; import { useUser, useUserProfile } from "@/hooks/store/user"; // services import { AuthService } from "@/services/auth.service"; type TProfileSetupFormValues = { first_name: string; last_name: string; avatar_url?: string | null; password?: string; confirm_password?: string; role?: string; use_case?: string; }; const defaultValues: Partial = { first_name: "", last_name: "", avatar_url: "", 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 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 [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false); const [showPassword, setShowPassword] = useState({ password: false, retypePassword: false, }); // plane hooks const { t } = useTranslation(); // store hooks const { updateCurrentUser } = useUser(); const { updateUserProfile } = useUserProfile(); // 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_url: user?.avatar_url, }, mode: "onChange", }); // derived values const userAvatar = watch("avatar_url"); 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 }) .then(() => { captureSuccess({ eventName: AUTH_TRACKER_EVENTS.password_created, }); }) .catch(() => { captureError({ eventName: AUTH_TRACKER_EVENTS.password_created, }); }); }; const handleSubmitProfileSetup = async (formData: TProfileSetupFormValues) => { const userDetailsPayload: Partial = { first_name: formData.first_name, last_name: formData.last_name, avatar_url: formData.avatar_url ?? undefined, }; 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 }), ]); captureSuccess({ eventName: USER_TRACKER_EVENTS.add_details, payload: { use_case: formData.use_case, role: formData.role, }, }); 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 { captureError({ eventName: USER_TRACKER_EVENTS.add_details, }); 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_url: formData.avatar_url ?? undefined, }; try { await Promise.all([ updateCurrentUser(userDetailsPayload), formData.password && handleSetPassword(formData.password), ]).then(() => { if (formData.password) { captureView({ elementName: ONBOARDING_TRACKER_ELEMENTS.PASSWORD_CREATION_SELECTED, }); } else { captureView({ elementName: ONBOARDING_TRACKER_ELEMENTS.PASSWORD_CREATION_SKIPPED, }); } setProfileSetupStep(EProfileSetupSteps.USER_PERSONALIZATION); }); } catch { captureError({ eventName: USER_TRACKER_EVENTS.add_details, }); 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 }), ]); captureSuccess({ eventName: USER_TRACKER_EVENTS.add_details, payload: { use_case: formData.use_case, role: formData.role, }, }); 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 { captureError({ eventName: USER_TRACKER_EVENTS.add_details, }); setToast({ type: TOAST_TYPE.ERROR, title: "Error", message: "Profile setup failed. Please try again!", }); } }; const onSubmit = async (formData: TProfileSetupFormValues) => { if (!user) return; captureView({ elementName: ONBOARDING_TRACKER_ELEMENTS.PROFILE_SETUP_FORM, }); 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; setValue("avatar_url", ""); }; // derived values const isPasswordAlreadySetup = !user?.is_password_autoset; const currentPassword = watch("password") || undefined; const currentConfirmPassword = watch("confirm_password") || undefined; const isValidPassword = useMemo(() => { if (currentPassword) { if ( currentPassword === currentConfirmPassword && getPasswordStrength(currentPassword) === E_PASSWORD_STRENGTH.STRENGTH_VALID ) { return true; } else { return false; } } else { return true; } }, [currentPassword, currentConfirmPassword]); // 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 = !isSubmitting && isValid ? (isPasswordAlreadySetup ? false : isValidPassword ? false : true) : true; return (
{profileSetupStep !== EProfileSetupSteps.USER_PERSONALIZATION && ( <> ( setIsImageUploadModalOpen(false)} handleRemove={async () => handleDelete(getValues("avatar_url"))} 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}}
{/* setting up password for the first time */} {!isPasswordAlreadySetup && ( <>
(
setIsPasswordInputFocused(true)} onBlur={() => setIsPasswordInputFocused(false)} autoComplete="on" /> {showPassword.password ? ( handleShowPassword("password")} /> ) : ( handleShowPassword("password")} /> )}
)} />
watch("password") ? (value === watch("password") ? true : "Passwords don't match") : true, }} render={({ field: { value, onChange, ref } }) => (
{showPassword.retypePassword ? ( handleShowPassword("retypePassword")} /> ) : ( handleShowPassword("retypePassword")} /> )}
)} /> {errors.confirm_password && ( {errors.confirm_password.message} )}
)} )} {/* user role once the password is set */} {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}}
)}
); });