[WEB-1691] chore: refactor finish onboarding logic to avoid multiple redirections. (#4950)
* [WEB-1691] chore: refactor finsh onboarding logic to avoid multiple redirections. * fix: infinite redirection on visiting onboarding page when the user is not authenticated. * chore: update intercepter redirect logic.
This commit is contained in:
parent
1f9f821543
commit
ff4de9ac11
6 changed files with 91 additions and 88 deletions
|
|
@ -5,6 +5,8 @@ import { observer } from "mobx-react";
|
|||
import useSWR from "swr";
|
||||
// types
|
||||
import { TOnboardingSteps, TUserProfile } from "@plane/types";
|
||||
// ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { InviteMembers, CreateOrJoinWorkspaces, ProfileSetup } from "@/components/onboarding";
|
||||
|
|
@ -14,8 +16,7 @@ import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
|
|||
// helpers
|
||||
import { EPageTypes } from "@/helpers/authentication.helper";
|
||||
// hooks
|
||||
import { useUser, useWorkspace, useUserProfile, useEventTracker, useUserSettings } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { useUser, useWorkspace, useUserProfile, useEventTracker } from "@/hooks/store";
|
||||
// wrappers
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers";
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
|
|
@ -33,24 +34,33 @@ const OnboardingPage = observer(() => {
|
|||
// states
|
||||
const [step, setStep] = useState<EOnboardingSteps | null>(null);
|
||||
const [totalSteps, setTotalSteps] = useState<number | null>(null);
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
// store hooks
|
||||
const { captureEvent } = useEventTracker();
|
||||
const { isLoading: userLoader, data: user, updateCurrentUser } = useUser();
|
||||
const { data: profile, updateUserOnBoard, updateUserProfile } = useUserProfile();
|
||||
const { data: currentUserSettings } = useUserSettings();
|
||||
const { data: profile, updateUserProfile, finishUserOnboarding } = useUserProfile();
|
||||
const { workspaces, fetchWorkspaces } = useWorkspace();
|
||||
|
||||
// computed values
|
||||
const workspacesList = Object.values(workspaces ?? {});
|
||||
// fetching workspaces list
|
||||
const { isLoading: workspaceListLoader } = useSWR(USER_WORKSPACES_LIST, () => fetchWorkspaces(), {
|
||||
const { isLoading: workspaceListLoader } = useSWR(
|
||||
USER_WORKSPACES_LIST,
|
||||
() => {
|
||||
user?.id && fetchWorkspaces();
|
||||
},
|
||||
{
|
||||
shouldRetryOnError: false,
|
||||
});
|
||||
}
|
||||
);
|
||||
// fetching user workspace invitations
|
||||
const { data: invitations } = useSWR("USER_WORKSPACE_INVITATIONS_LIST", () =>
|
||||
workspaceService.userWorkspaceInvitations()
|
||||
const { data: invitations } = useSWR(
|
||||
"USER_WORKSPACE_INVITATIONS_LIST",
|
||||
() => {
|
||||
user?.id && workspaceService.userWorkspaceInvitations();
|
||||
},
|
||||
{
|
||||
shouldRetryOnError: false,
|
||||
}
|
||||
);
|
||||
// handle step change
|
||||
const stepChange = async (steps: Partial<TOnboardingSteps>) => {
|
||||
|
|
@ -66,45 +76,11 @@ const OnboardingPage = observer(() => {
|
|||
await updateUserProfile(payload);
|
||||
};
|
||||
|
||||
const getWorkspaceRedirectionUrl = (): string => {
|
||||
let redirectionRoute = "/profile";
|
||||
|
||||
// validate the last and fallback workspace_slug
|
||||
const currentWorkspaceSlug =
|
||||
currentUserSettings?.workspace?.last_workspace_slug ||
|
||||
currentUserSettings?.workspace?.fallback_workspace_slug ||
|
||||
undefined;
|
||||
|
||||
if (currentWorkspaceSlug) {
|
||||
const isCurrentWorkspaceValid = Object.values(workspaces || {}).findIndex(
|
||||
(workspace) => workspace.slug === currentWorkspaceSlug
|
||||
);
|
||||
if (isCurrentWorkspaceValid >= 0) {
|
||||
redirectionRoute = `/${currentWorkspaceSlug}`;
|
||||
}
|
||||
}
|
||||
|
||||
return redirectionRoute;
|
||||
};
|
||||
|
||||
// complete onboarding
|
||||
const finishOnboarding = async () => {
|
||||
if (!user || !workspaces) return;
|
||||
if (!user) return;
|
||||
|
||||
const firstWorkspace = Object.values(workspaces ?? {})?.[0];
|
||||
|
||||
await Promise.all([
|
||||
updateUserProfile({
|
||||
onboarding_step: {
|
||||
profile_complete: true,
|
||||
workspace_join: true,
|
||||
workspace_create: true,
|
||||
workspace_invite: true,
|
||||
},
|
||||
last_workspace_id: firstWorkspace?.id,
|
||||
}),
|
||||
updateUserOnBoard(),
|
||||
])
|
||||
await finishUserOnboarding()
|
||||
.then(() => {
|
||||
captureEvent(USER_ONBOARDING_COMPLETED, {
|
||||
// user_role: user.role,
|
||||
|
|
@ -114,10 +90,12 @@ const OnboardingPage = observer(() => {
|
|||
});
|
||||
})
|
||||
.catch(() => {
|
||||
console.log("Failed to update onboarding status");
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Failed",
|
||||
message: "Failed to finish onboarding, Please try again later.",
|
||||
});
|
||||
});
|
||||
|
||||
router.replace(`${getWorkspaceRedirectionUrl()}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { ROLE } from "@/constants/workspace";
|
|||
import { truncateText } from "@/helpers/string.helper";
|
||||
import { getUserRole } from "@/helpers/user.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useWorkspace } from "@/hooks/store";
|
||||
import { useEventTracker, useUserSettings, useWorkspace } from "@/hooks/store";
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
|
||||
|
|
@ -32,6 +32,7 @@ export const Invitations: React.FC<Props> = (props) => {
|
|||
// store hooks
|
||||
const { captureEvent } = useEventTracker();
|
||||
const { fetchWorkspaces } = useWorkspace();
|
||||
const { fetchCurrentUserSettings } = useUserSettings();
|
||||
|
||||
const { data: invitations } = useSWR(USER_WORKSPACE_INVITATIONS, () => workspaceService.userWorkspaceInvitations());
|
||||
|
||||
|
|
@ -61,6 +62,7 @@ export const Invitations: React.FC<Props> = (props) => {
|
|||
element: "Workspace invitations page",
|
||||
});
|
||||
await fetchWorkspaces();
|
||||
await fetchCurrentUserSettings();
|
||||
await handleNextStep();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ export const AuthenticationWrapper: FC<TAuthenticationWrapper> = observer((props
|
|||
} else {
|
||||
if (currentUser && currentUserProfile?.id && isUserOnboard) {
|
||||
const currentRedirectRoute = getWorkspaceRedirectionUrl();
|
||||
router.push(currentRedirectRoute);
|
||||
router.replace(currentRedirectRoute);
|
||||
return <></>;
|
||||
} else return <>{children}</>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ export abstract class APIService {
|
|||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response && error.response.status === 401) {
|
||||
window.location.reload();
|
||||
const currentPath = window.location.pathname;
|
||||
window.location.replace(`/${currentPath ? `?next_path=${currentPath}` : ``}`);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export interface IUserProfileStore {
|
|||
// actions
|
||||
fetchUserProfile: () => Promise<TUserProfile | undefined>;
|
||||
updateUserProfile: (data: Partial<TUserProfile>) => Promise<TUserProfile | undefined>;
|
||||
updateUserOnBoard: () => Promise<TUserProfile | undefined>;
|
||||
finishUserOnboarding: () => Promise<void>;
|
||||
updateTourCompleted: () => Promise<TUserProfile | undefined>;
|
||||
updateUserTheme: (data: Partial<IUserTheme>) => Promise<TUserProfile | undefined>;
|
||||
}
|
||||
|
|
@ -72,7 +72,6 @@ export class ProfileStore implements IUserProfileStore {
|
|||
// actions
|
||||
fetchUserProfile: action,
|
||||
updateUserProfile: action,
|
||||
updateUserOnBoard: action,
|
||||
updateTourCompleted: action,
|
||||
updateUserTheme: action,
|
||||
});
|
||||
|
|
@ -80,12 +79,20 @@ export class ProfileStore implements IUserProfileStore {
|
|||
this.userService = new UserService();
|
||||
}
|
||||
|
||||
// helper action
|
||||
mutateUserProfile = (data: Partial<TUserProfile>) => {
|
||||
if (!data) return
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (key in this.data) set(this.data, key, value);
|
||||
})
|
||||
}
|
||||
|
||||
// actions
|
||||
/**
|
||||
* @description fetches user profile information
|
||||
* @returns {Promise<TUserProfile | undefined>}
|
||||
*/
|
||||
fetchUserProfile = async () => {
|
||||
fetchUserProfile = async (): Promise<TUserProfile | undefined> => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.isLoading = true;
|
||||
|
|
@ -114,23 +121,17 @@ export class ProfileStore implements IUserProfileStore {
|
|||
* @param {Partial<TUserProfile>} data
|
||||
* @returns {Promise<TUserProfile | undefined>}
|
||||
*/
|
||||
updateUserProfile = async (data: Partial<TUserProfile>) => {
|
||||
updateUserProfile = async (data: Partial<TUserProfile>): Promise<TUserProfile | undefined> => {
|
||||
const currentUserProfileData = this.data;
|
||||
try {
|
||||
if (currentUserProfileData) {
|
||||
Object.keys(data).forEach((key: string) => {
|
||||
const userKey: keyof TUserProfile = key as keyof TUserProfile;
|
||||
if (this.data) set(this.data, userKey, data[userKey]);
|
||||
});
|
||||
this.mutateUserProfile(data);
|
||||
}
|
||||
const userProfile = await this.userService.updateCurrentUserProfile(data);
|
||||
return userProfile;
|
||||
} catch (error) {
|
||||
if (currentUserProfileData) {
|
||||
Object.keys(currentUserProfileData).forEach((key: string) => {
|
||||
const userKey: keyof TUserProfile = key as keyof TUserProfile;
|
||||
if (this.data) set(this.data, userKey, currentUserProfileData[userKey]);
|
||||
});
|
||||
this.mutateUserProfile(currentUserProfileData);
|
||||
}
|
||||
runInAction(() => {
|
||||
this.error = {
|
||||
|
|
@ -142,27 +143,43 @@ export class ProfileStore implements IUserProfileStore {
|
|||
};
|
||||
|
||||
/**
|
||||
* @description updates the user onboarding status
|
||||
* @returns @returns {Promise<TUserProfile | undefined>}
|
||||
* @description finishes the user onboarding
|
||||
* @returns { void }
|
||||
*/
|
||||
updateUserOnBoard = async () => {
|
||||
const isUserProfileOnboard = this.data.is_onboarded || false;
|
||||
finishUserOnboarding = async (): Promise<void> => {
|
||||
try {
|
||||
runInAction(() => set(this.data, ["is_onboarded"], true));
|
||||
const userProfile = await this.userService.updateUserOnBoard();
|
||||
return userProfile;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
set(this.data, ["is_onboarded"], isUserProfileOnboard);
|
||||
this.error = {
|
||||
status: "user-profile-onboard-error",
|
||||
message: "Failed to update user profile is_onboarded",
|
||||
const firstWorkspace = Object.values(this.store.workspaceRoot.workspaces ?? {})?.[0];
|
||||
const dataToUpdate: Partial<TUserProfile> = {
|
||||
onboarding_step: {
|
||||
profile_complete: true,
|
||||
workspace_join: true,
|
||||
workspace_create: true,
|
||||
workspace_invite: true,
|
||||
},
|
||||
last_workspace_id: firstWorkspace?.id,
|
||||
};
|
||||
|
||||
// update user onboarding steps
|
||||
await this.userService.updateCurrentUserProfile(dataToUpdate);
|
||||
|
||||
// update user onboarding status
|
||||
await this.userService.updateUserOnBoard();
|
||||
|
||||
// update the user profile store
|
||||
runInAction(() => {
|
||||
this.mutateUserProfile({ ...dataToUpdate, is_onboarded: true });
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.error = {
|
||||
status: "user-profile-onboard-finish-error",
|
||||
message: "Failed to finish user onboarding",
|
||||
};
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description updates the user tour completed status
|
||||
|
|
@ -171,12 +188,12 @@ export class ProfileStore implements IUserProfileStore {
|
|||
updateTourCompleted = async () => {
|
||||
const isUserProfileTourCompleted = this.data.is_tour_completed || false;
|
||||
try {
|
||||
runInAction(() => set(this.data, ["is_tour_completed"], true));
|
||||
this.mutateUserProfile({ is_tour_completed: true });
|
||||
const userProfile = await this.userService.updateUserTourCompleted();
|
||||
return userProfile;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
set(this.data, ["is_tour_completed"], isUserProfileTourCompleted);
|
||||
this.mutateUserProfile({ is_tour_completed: isUserProfileTourCompleted });
|
||||
this.error = {
|
||||
status: "user-profile-tour-complete-error",
|
||||
message: "Failed to update user profile is_tour_completed",
|
||||
|
|
|
|||
|
|
@ -111,14 +111,19 @@ export class WorkspaceRootStore implements IWorkspaceRootStore {
|
|||
*/
|
||||
fetchWorkspaces = async () => {
|
||||
this.loader = true;
|
||||
try {
|
||||
const workspaceResponse = await this.workspaceService.userWorkspaces();
|
||||
runInAction(() => {
|
||||
workspaceResponse.forEach((workspace) => {
|
||||
set(this.workspaces, [workspace.id], workspace);
|
||||
});
|
||||
});
|
||||
this.loader = false;
|
||||
return workspaceResponse;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
} finally {
|
||||
this.loader = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue