[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";
// 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(() => {

View file

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

View file

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

View file

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

View file

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

View file

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