[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:
Prateek Shourya 2024-06-27 18:46:25 +05:30 committed by GitHub
parent 1f9f821543
commit ff4de9ac11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 91 additions and 88 deletions

View file

@ -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(() => {

View file

@ -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);

View file

@ -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}</>;
} }

View file

@ -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);
} }

View file

@ -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",

View file

@ -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;
}
}; };
/** /**