[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";
|
import useSWR from "swr";
|
||||||
// types
|
// types
|
||||||
import { TOnboardingSteps, TUserProfile } from "@plane/types";
|
import { TOnboardingSteps, TUserProfile } from "@plane/types";
|
||||||
|
// ui
|
||||||
|
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { LogoSpinner } from "@/components/common";
|
import { LogoSpinner } from "@/components/common";
|
||||||
import { InviteMembers, CreateOrJoinWorkspaces, ProfileSetup } from "@/components/onboarding";
|
import { InviteMembers, CreateOrJoinWorkspaces, ProfileSetup } from "@/components/onboarding";
|
||||||
|
|
@ -14,8 +16,7 @@ import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
|
||||||
// helpers
|
// helpers
|
||||||
import { EPageTypes } from "@/helpers/authentication.helper";
|
import { EPageTypes } from "@/helpers/authentication.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser, useWorkspace, useUserProfile, useEventTracker, useUserSettings } from "@/hooks/store";
|
import { useUser, useWorkspace, useUserProfile, useEventTracker } from "@/hooks/store";
|
||||||
import { useAppRouter } from "@/hooks/use-app-router";
|
|
||||||
// wrappers
|
// wrappers
|
||||||
import { AuthenticationWrapper } from "@/lib/wrappers";
|
import { AuthenticationWrapper } from "@/lib/wrappers";
|
||||||
import { WorkspaceService } from "@/plane-web/services";
|
import { WorkspaceService } from "@/plane-web/services";
|
||||||
|
|
@ -33,24 +34,33 @@ const OnboardingPage = observer(() => {
|
||||||
// states
|
// states
|
||||||
const [step, setStep] = useState<EOnboardingSteps | null>(null);
|
const [step, setStep] = useState<EOnboardingSteps | null>(null);
|
||||||
const [totalSteps, setTotalSteps] = useState<number | null>(null);
|
const [totalSteps, setTotalSteps] = useState<number | null>(null);
|
||||||
// router
|
|
||||||
const router = useAppRouter();
|
|
||||||
// store hooks
|
// store hooks
|
||||||
const { captureEvent } = useEventTracker();
|
const { captureEvent } = useEventTracker();
|
||||||
const { isLoading: userLoader, data: user, updateCurrentUser } = useUser();
|
const { isLoading: userLoader, data: user, updateCurrentUser } = useUser();
|
||||||
const { data: profile, updateUserOnBoard, updateUserProfile } = useUserProfile();
|
const { data: profile, updateUserProfile, finishUserOnboarding } = useUserProfile();
|
||||||
const { data: currentUserSettings } = useUserSettings();
|
|
||||||
const { workspaces, fetchWorkspaces } = useWorkspace();
|
const { workspaces, fetchWorkspaces } = useWorkspace();
|
||||||
|
|
||||||
// computed values
|
// computed values
|
||||||
const workspacesList = Object.values(workspaces ?? {});
|
const workspacesList = Object.values(workspaces ?? {});
|
||||||
// fetching workspaces list
|
// fetching workspaces list
|
||||||
const { isLoading: workspaceListLoader } = useSWR(USER_WORKSPACES_LIST, () => fetchWorkspaces(), {
|
const { isLoading: workspaceListLoader } = useSWR(
|
||||||
shouldRetryOnError: false,
|
USER_WORKSPACES_LIST,
|
||||||
});
|
() => {
|
||||||
|
user?.id && fetchWorkspaces();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shouldRetryOnError: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
// fetching user workspace invitations
|
// fetching user workspace invitations
|
||||||
const { data: invitations } = useSWR("USER_WORKSPACE_INVITATIONS_LIST", () =>
|
const { data: invitations } = useSWR(
|
||||||
workspaceService.userWorkspaceInvitations()
|
"USER_WORKSPACE_INVITATIONS_LIST",
|
||||||
|
() => {
|
||||||
|
user?.id && workspaceService.userWorkspaceInvitations();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shouldRetryOnError: false,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
// handle step change
|
// handle step change
|
||||||
const stepChange = async (steps: Partial<TOnboardingSteps>) => {
|
const stepChange = async (steps: Partial<TOnboardingSteps>) => {
|
||||||
|
|
@ -66,45 +76,11 @@ const OnboardingPage = observer(() => {
|
||||||
await updateUserProfile(payload);
|
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
|
// complete onboarding
|
||||||
const finishOnboarding = async () => {
|
const finishOnboarding = async () => {
|
||||||
if (!user || !workspaces) return;
|
if (!user) return;
|
||||||
|
|
||||||
const firstWorkspace = Object.values(workspaces ?? {})?.[0];
|
await finishUserOnboarding()
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
updateUserProfile({
|
|
||||||
onboarding_step: {
|
|
||||||
profile_complete: true,
|
|
||||||
workspace_join: true,
|
|
||||||
workspace_create: true,
|
|
||||||
workspace_invite: true,
|
|
||||||
},
|
|
||||||
last_workspace_id: firstWorkspace?.id,
|
|
||||||
}),
|
|
||||||
updateUserOnBoard(),
|
|
||||||
])
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
captureEvent(USER_ONBOARDING_COMPLETED, {
|
captureEvent(USER_ONBOARDING_COMPLETED, {
|
||||||
// user_role: user.role,
|
// user_role: user.role,
|
||||||
|
|
@ -114,10 +90,12 @@ const OnboardingPage = observer(() => {
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.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(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { ROLE } from "@/constants/workspace";
|
||||||
import { truncateText } from "@/helpers/string.helper";
|
import { truncateText } from "@/helpers/string.helper";
|
||||||
import { getUserRole } from "@/helpers/user.helper";
|
import { getUserRole } from "@/helpers/user.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useEventTracker, useWorkspace } from "@/hooks/store";
|
import { useEventTracker, useUserSettings, useWorkspace } from "@/hooks/store";
|
||||||
// services
|
// services
|
||||||
import { WorkspaceService } from "@/plane-web/services";
|
import { WorkspaceService } from "@/plane-web/services";
|
||||||
|
|
||||||
|
|
@ -32,6 +32,7 @@ export const Invitations: React.FC<Props> = (props) => {
|
||||||
// store hooks
|
// store hooks
|
||||||
const { captureEvent } = useEventTracker();
|
const { captureEvent } = useEventTracker();
|
||||||
const { fetchWorkspaces } = useWorkspace();
|
const { fetchWorkspaces } = useWorkspace();
|
||||||
|
const { fetchCurrentUserSettings } = useUserSettings();
|
||||||
|
|
||||||
const { data: invitations } = useSWR(USER_WORKSPACE_INVITATIONS, () => workspaceService.userWorkspaceInvitations());
|
const { data: invitations } = useSWR(USER_WORKSPACE_INVITATIONS, () => workspaceService.userWorkspaceInvitations());
|
||||||
|
|
||||||
|
|
@ -61,6 +62,7 @@ export const Invitations: React.FC<Props> = (props) => {
|
||||||
element: "Workspace invitations page",
|
element: "Workspace invitations page",
|
||||||
});
|
});
|
||||||
await fetchWorkspaces();
|
await fetchWorkspaces();
|
||||||
|
await fetchCurrentUserSettings();
|
||||||
await handleNextStep();
|
await handleNextStep();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ export const AuthenticationWrapper: FC<TAuthenticationWrapper> = observer((props
|
||||||
} else {
|
} else {
|
||||||
if (currentUser && currentUserProfile?.id && isUserOnboard) {
|
if (currentUser && currentUserProfile?.id && isUserOnboard) {
|
||||||
const currentRedirectRoute = getWorkspaceRedirectionUrl();
|
const currentRedirectRoute = getWorkspaceRedirectionUrl();
|
||||||
router.push(currentRedirectRoute);
|
router.replace(currentRedirectRoute);
|
||||||
return <></>;
|
return <></>;
|
||||||
} else return <>{children}</>;
|
} else return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ export abstract class APIService {
|
||||||
(response) => response,
|
(response) => response,
|
||||||
(error) => {
|
(error) => {
|
||||||
if (error.response && error.response.status === 401) {
|
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);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export interface IUserProfileStore {
|
||||||
// actions
|
// actions
|
||||||
fetchUserProfile: () => Promise<TUserProfile | undefined>;
|
fetchUserProfile: () => Promise<TUserProfile | undefined>;
|
||||||
updateUserProfile: (data: Partial<TUserProfile>) => Promise<TUserProfile | undefined>;
|
updateUserProfile: (data: Partial<TUserProfile>) => Promise<TUserProfile | undefined>;
|
||||||
updateUserOnBoard: () => Promise<TUserProfile | undefined>;
|
finishUserOnboarding: () => Promise<void>;
|
||||||
updateTourCompleted: () => Promise<TUserProfile | undefined>;
|
updateTourCompleted: () => Promise<TUserProfile | undefined>;
|
||||||
updateUserTheme: (data: Partial<IUserTheme>) => Promise<TUserProfile | undefined>;
|
updateUserTheme: (data: Partial<IUserTheme>) => Promise<TUserProfile | undefined>;
|
||||||
}
|
}
|
||||||
|
|
@ -72,7 +72,6 @@ export class ProfileStore implements IUserProfileStore {
|
||||||
// actions
|
// actions
|
||||||
fetchUserProfile: action,
|
fetchUserProfile: action,
|
||||||
updateUserProfile: action,
|
updateUserProfile: action,
|
||||||
updateUserOnBoard: action,
|
|
||||||
updateTourCompleted: action,
|
updateTourCompleted: action,
|
||||||
updateUserTheme: action,
|
updateUserTheme: action,
|
||||||
});
|
});
|
||||||
|
|
@ -80,12 +79,20 @@ export class ProfileStore implements IUserProfileStore {
|
||||||
this.userService = new UserService();
|
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
|
// actions
|
||||||
/**
|
/**
|
||||||
* @description fetches user profile information
|
* @description fetches user profile information
|
||||||
* @returns {Promise<TUserProfile | undefined>}
|
* @returns {Promise<TUserProfile | undefined>}
|
||||||
*/
|
*/
|
||||||
fetchUserProfile = async () => {
|
fetchUserProfile = async (): Promise<TUserProfile | undefined> => {
|
||||||
try {
|
try {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
@ -114,23 +121,17 @@ export class ProfileStore implements IUserProfileStore {
|
||||||
* @param {Partial<TUserProfile>} data
|
* @param {Partial<TUserProfile>} data
|
||||||
* @returns {Promise<TUserProfile | undefined>}
|
* @returns {Promise<TUserProfile | undefined>}
|
||||||
*/
|
*/
|
||||||
updateUserProfile = async (data: Partial<TUserProfile>) => {
|
updateUserProfile = async (data: Partial<TUserProfile>): Promise<TUserProfile | undefined> => {
|
||||||
const currentUserProfileData = this.data;
|
const currentUserProfileData = this.data;
|
||||||
try {
|
try {
|
||||||
if (currentUserProfileData) {
|
if (currentUserProfileData) {
|
||||||
Object.keys(data).forEach((key: string) => {
|
this.mutateUserProfile(data);
|
||||||
const userKey: keyof TUserProfile = key as keyof TUserProfile;
|
|
||||||
if (this.data) set(this.data, userKey, data[userKey]);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
const userProfile = await this.userService.updateCurrentUserProfile(data);
|
const userProfile = await this.userService.updateCurrentUserProfile(data);
|
||||||
return userProfile;
|
return userProfile;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (currentUserProfileData) {
|
if (currentUserProfileData) {
|
||||||
Object.keys(currentUserProfileData).forEach((key: string) => {
|
this.mutateUserProfile(currentUserProfileData);
|
||||||
const userKey: keyof TUserProfile = key as keyof TUserProfile;
|
|
||||||
if (this.data) set(this.data, userKey, currentUserProfileData[userKey]);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.error = {
|
this.error = {
|
||||||
|
|
@ -142,27 +143,43 @@ export class ProfileStore implements IUserProfileStore {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description updates the user onboarding status
|
* @description finishes the user onboarding
|
||||||
* @returns @returns {Promise<TUserProfile | undefined>}
|
* @returns { void }
|
||||||
*/
|
*/
|
||||||
updateUserOnBoard = async () => {
|
finishUserOnboarding = async (): Promise<void> => {
|
||||||
const isUserProfileOnboard = this.data.is_onboarded || false;
|
|
||||||
try {
|
try {
|
||||||
runInAction(() => set(this.data, ["is_onboarded"], true));
|
const firstWorkspace = Object.values(this.store.workspaceRoot.workspaces ?? {})?.[0];
|
||||||
const userProfile = await this.userService.updateUserOnBoard();
|
const dataToUpdate: Partial<TUserProfile> = {
|
||||||
return userProfile;
|
onboarding_step: {
|
||||||
} catch (error) {
|
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(() => {
|
runInAction(() => {
|
||||||
set(this.data, ["is_onboarded"], isUserProfileOnboard);
|
this.mutateUserProfile({ ...dataToUpdate, is_onboarded: true });
|
||||||
this.error = {
|
|
||||||
status: "user-profile-onboard-error",
|
|
||||||
message: "Failed to update user profile is_onboarded",
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.error = {
|
||||||
|
status: "user-profile-onboard-finish-error",
|
||||||
|
message: "Failed to finish user onboarding",
|
||||||
|
};
|
||||||
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description updates the user tour completed status
|
* @description updates the user tour completed status
|
||||||
|
|
@ -171,12 +188,12 @@ export class ProfileStore implements IUserProfileStore {
|
||||||
updateTourCompleted = async () => {
|
updateTourCompleted = async () => {
|
||||||
const isUserProfileTourCompleted = this.data.is_tour_completed || false;
|
const isUserProfileTourCompleted = this.data.is_tour_completed || false;
|
||||||
try {
|
try {
|
||||||
runInAction(() => set(this.data, ["is_tour_completed"], true));
|
this.mutateUserProfile({ is_tour_completed: true });
|
||||||
const userProfile = await this.userService.updateUserTourCompleted();
|
const userProfile = await this.userService.updateUserTourCompleted();
|
||||||
return userProfile;
|
return userProfile;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
set(this.data, ["is_tour_completed"], isUserProfileTourCompleted);
|
this.mutateUserProfile({ is_tour_completed: isUserProfileTourCompleted });
|
||||||
this.error = {
|
this.error = {
|
||||||
status: "user-profile-tour-complete-error",
|
status: "user-profile-tour-complete-error",
|
||||||
message: "Failed to update user profile is_tour_completed",
|
message: "Failed to update user profile is_tour_completed",
|
||||||
|
|
|
||||||
|
|
@ -111,14 +111,19 @@ export class WorkspaceRootStore implements IWorkspaceRootStore {
|
||||||
*/
|
*/
|
||||||
fetchWorkspaces = async () => {
|
fetchWorkspaces = async () => {
|
||||||
this.loader = true;
|
this.loader = true;
|
||||||
const workspaceResponse = await this.workspaceService.userWorkspaces();
|
try {
|
||||||
runInAction(() => {
|
const workspaceResponse = await this.workspaceService.userWorkspaces();
|
||||||
workspaceResponse.forEach((workspace) => {
|
runInAction(() => {
|
||||||
set(this.workspaces, [workspace.id], workspace);
|
workspaceResponse.forEach((workspace) => {
|
||||||
|
set(this.workspaces, [workspace.id], workspace);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
return workspaceResponse;
|
||||||
this.loader = false;
|
} catch (e) {
|
||||||
return workspaceResponse;
|
throw e;
|
||||||
|
} finally {
|
||||||
|
this.loader = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue