* refactor: apply biome formatting to apps/web (batch 1) Formats apps/web non-modules directories: - apps/web/app - apps/web/lib - apps/web/pages - apps/web/styles - apps/web/server - apps/web/test - Root-level .ts and .mjs files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web/modules (batch 2) Formats smaller apps/web/modules directories: - onboarding, data-table, users, apps, auth - integration-attribute-sync, shell, availability - feature-flags, team, webhooks, getting-started - feature-opt-in, troubleshooter, schedules, notifications - signup-view.tsx Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web/modules (batch 3) Formats medium apps/web/modules directories: - bookings - event-types - insights - embed Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web/modules/ee (batch 4) Formats apps/web/modules/ee directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web (batch 5) Formats remaining apps/web directories: - apps/web/modules/settings - apps/web/modules/booking-audit - apps/web/playwright Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web/components (batch 6) Formats remaining apps/web/components files. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to new apps/web files from main Formats newly added/modified files from main merge: - WorkflowStepContainer.tsx (resolved merge conflicts) - Newly moved files (blocklist, form-builder, schedules, etc.) - Other files modified in main Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to new apps/web files from main Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
594 lines
17 KiB
TypeScript
594 lines
17 KiB
TypeScript
import type { GetServerSidePropsContext, GetServerSidePropsResult } from "next";
|
|
import { z } from "zod";
|
|
|
|
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
|
|
import { filterEventTypesWhereLocationUpdateIsAllowed } from "@calcom/app-store/_utils/getBulkEventTypes";
|
|
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
|
import type { LocationObject } from "@calcom/app-store/locations";
|
|
import { isConferencing as isConferencingApp } from "@calcom/app-store/utils";
|
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
|
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
|
import { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps";
|
|
import { CAL_URL } from "@calcom/lib/constants";
|
|
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
|
import prisma from "@calcom/prisma";
|
|
import { Prisma } from "@calcom/prisma/client";
|
|
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
|
|
|
import { STEPS } from "../../../../modules/apps/installation/[[...step]]/constants";
|
|
import type {
|
|
OnboardingPageProps,
|
|
TEventTypeGroup,
|
|
TEventType,
|
|
} from "../../../../modules/apps/installation/[[...step]]/step-view";
|
|
|
|
const eventTypeSelect: Prisma.EventTypeSelect = {
|
|
id: true,
|
|
description: true,
|
|
durationLimits: true,
|
|
metadata: true,
|
|
length: true,
|
|
title: true,
|
|
position: true,
|
|
recurringEvent: true,
|
|
requiresConfirmation: true,
|
|
canSendCalVideoTranscriptionEmails: true,
|
|
team: { select: { slug: true } },
|
|
schedulingType: true,
|
|
teamId: true,
|
|
users: { select: { username: true } },
|
|
seatsPerTimeSlot: true,
|
|
slug: true,
|
|
locations: true,
|
|
userId: true,
|
|
destinationCalendar: true,
|
|
bookingFields: true,
|
|
calVideoSettings: true,
|
|
parentId: true,
|
|
};
|
|
|
|
type EventTypeFromDb = Prisma.EventTypeGetPayload<{
|
|
select: typeof eventTypeSelect;
|
|
}>;
|
|
|
|
type MinimalUser = {
|
|
id: number;
|
|
username: string | null;
|
|
name: string | null;
|
|
avatarUrl: string | null;
|
|
};
|
|
|
|
type MinimalTeam = {
|
|
id: number;
|
|
name: string;
|
|
logoUrl: string | null;
|
|
isOrganization: boolean;
|
|
};
|
|
|
|
type RedirectResult = { redirect: { permanent: boolean; destination: string } };
|
|
|
|
const mapEventType = (item: EventTypeFromDb): TEventType => {
|
|
let teamSlug = "";
|
|
if (item.team) {
|
|
teamSlug = `team/${item.team.slug}`;
|
|
}
|
|
const userSlug = item?.users?.[0]?.username;
|
|
let urlPart = userSlug;
|
|
if (teamSlug) {
|
|
urlPart = teamSlug;
|
|
}
|
|
|
|
return {
|
|
...item,
|
|
URL: `${CAL_URL}/${urlPart}/${item.slug}`,
|
|
selected: false,
|
|
locations: item.locations as unknown as LocationObject[],
|
|
bookingFields: eventTypeBookingFields.parse(item.bookingFields || []),
|
|
};
|
|
};
|
|
|
|
const getUser = async (
|
|
userId: number
|
|
): Promise<
|
|
| (MinimalUser & {
|
|
teams: {
|
|
id: number;
|
|
name: string;
|
|
logoUrl: string | null;
|
|
isOrganization: boolean;
|
|
parent: { id: number; name: string; logoUrl: string | null } | null;
|
|
}[];
|
|
})
|
|
| null
|
|
> => {
|
|
const userRepo = new UserRepository(prisma);
|
|
const userAdminTeams = await userRepo.getUserAdminTeams({ userId });
|
|
|
|
if (!userAdminTeams?.id) {
|
|
return null;
|
|
}
|
|
|
|
const teams = userAdminTeams.teams.map(({ team }) => {
|
|
const parentLogoUrl = team.parent?.logoUrl;
|
|
const parentName = team.parent?.name;
|
|
|
|
let logoUrl = "";
|
|
if (team.parent) {
|
|
logoUrl = getPlaceholderAvatar(parentLogoUrl, parentName);
|
|
} else {
|
|
logoUrl = getPlaceholderAvatar(team.logoUrl, team.name);
|
|
}
|
|
return {
|
|
...team,
|
|
logoUrl,
|
|
};
|
|
});
|
|
|
|
return {
|
|
...userAdminTeams,
|
|
teams,
|
|
};
|
|
};
|
|
|
|
const getOrgSubTeams = async (
|
|
parentId: number
|
|
): Promise<
|
|
{
|
|
id: number;
|
|
name: string;
|
|
logoUrl: string | null;
|
|
isOrganization: boolean;
|
|
parent: { id: number; name: string; logoUrl: string | null } | null;
|
|
}[]
|
|
> => {
|
|
const teams = await prisma.team.findMany({
|
|
where: {
|
|
parentId,
|
|
},
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
logoUrl: true,
|
|
isOrganization: true,
|
|
parent: {
|
|
select: {
|
|
logoUrl: true,
|
|
name: true,
|
|
id: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
return teams.map((team) => {
|
|
const parentLogoUrl = team.parent?.logoUrl;
|
|
const parentName = team.parent?.name;
|
|
let logoUrl = "";
|
|
if (team.parent) {
|
|
logoUrl = getPlaceholderAvatar(parentLogoUrl, parentName);
|
|
} else {
|
|
logoUrl = getPlaceholderAvatar(team.logoUrl, team.name);
|
|
}
|
|
return {
|
|
...team,
|
|
logoUrl,
|
|
};
|
|
});
|
|
};
|
|
|
|
const getAppBySlug = async (
|
|
appSlug: string
|
|
): Promise<{ slug: string; keys: Prisma.JsonValue; enabled: boolean; dirName: string } | null> => {
|
|
const app = await prisma.app.findUnique({
|
|
where: { slug: appSlug, enabled: true },
|
|
select: { slug: true, keys: true, enabled: true, dirName: true },
|
|
});
|
|
return app;
|
|
};
|
|
|
|
const getTeamEventTypes = async (
|
|
teamIds: number[],
|
|
isConferencing: boolean,
|
|
eventTypeSelect: Prisma.EventTypeSelect
|
|
): Promise<TEventTypeGroup[]> => {
|
|
const teams = await prisma.team.findMany({
|
|
where: { id: { in: teamIds }, isOrganization: false },
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
logoUrl: true,
|
|
slug: true,
|
|
isOrganization: true,
|
|
eventTypes: { select: eventTypeSelect },
|
|
},
|
|
});
|
|
return teams.map((team) => {
|
|
let filteredEventTypes = team.eventTypes;
|
|
if (isConferencing) {
|
|
filteredEventTypes = filterEventTypesWhereLocationUpdateIsAllowed(team.eventTypes);
|
|
}
|
|
return {
|
|
teamId: team.id,
|
|
slug: team.slug,
|
|
name: team.name,
|
|
isOrganisation: team.isOrganization,
|
|
image: getPlaceholderAvatar(team.logoUrl, team.name),
|
|
eventTypes: (filteredEventTypes as unknown as EventTypeFromDb[])
|
|
.map(mapEventType)
|
|
.sort((a, b) => (b.position || 0) - (a.position || 0)),
|
|
};
|
|
});
|
|
};
|
|
|
|
const getUserEventTypes = async (
|
|
userId: number,
|
|
isConferencing: boolean,
|
|
eventTypeSelect: Prisma.EventTypeSelect
|
|
): Promise<TEventTypeGroup[]> => {
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: userId },
|
|
select: {
|
|
id: true,
|
|
username: true,
|
|
name: true,
|
|
avatarUrl: true,
|
|
eventTypes: { where: { teamId: null }, select: eventTypeSelect },
|
|
},
|
|
});
|
|
|
|
if (!user) return [];
|
|
|
|
let filteredEventTypes = user.eventTypes;
|
|
if (isConferencing) {
|
|
filteredEventTypes = filterEventTypesWhereLocationUpdateIsAllowed(user.eventTypes);
|
|
}
|
|
return [
|
|
{
|
|
userId: user.id,
|
|
slug: user.username,
|
|
name: user.name,
|
|
image: getPlaceholderAvatar(user.avatarUrl, user.name),
|
|
eventTypes: (filteredEventTypes as unknown as EventTypeFromDb[])
|
|
.map(mapEventType)
|
|
.sort((a, b) => (b.position || 0) - (a.position || 0)),
|
|
},
|
|
];
|
|
};
|
|
|
|
const getEventTypes = async ({
|
|
userId,
|
|
teamIds,
|
|
isConferencing = false,
|
|
}: {
|
|
userId: number;
|
|
teamIds?: number[];
|
|
isConferencing?: boolean;
|
|
}): Promise<TEventTypeGroup[] | null> => {
|
|
if (teamIds && teamIds.length > 0) {
|
|
return getTeamEventTypes(teamIds, isConferencing, eventTypeSelect);
|
|
}
|
|
|
|
return getUserEventTypes(userId, isConferencing, eventTypeSelect);
|
|
};
|
|
|
|
const getAppInstallsBySlug = async (
|
|
appSlug: string,
|
|
userId: number,
|
|
teamIds?: number[]
|
|
): Promise<Prisma.CredentialGetPayload<Record<string, never>>[]> => {
|
|
if (teamIds?.length) {
|
|
return prisma.credential.findMany({
|
|
where: {
|
|
OR: [
|
|
{ appId: appSlug, userId: userId },
|
|
{ appId: appSlug, teamId: { in: teamIds } },
|
|
],
|
|
},
|
|
});
|
|
}
|
|
|
|
return prisma.credential.findMany({
|
|
where: { appId: appSlug, userId: userId },
|
|
});
|
|
};
|
|
|
|
const handleAutoInstall = async (
|
|
user: MinimalUser,
|
|
appMetadata: import("@calcom/types/App").AppMeta,
|
|
parsedAppSlug: string
|
|
): Promise<number | null> => {
|
|
try {
|
|
const newCredential = await createDefaultInstallation({
|
|
appType: appMetadata.type,
|
|
user: { id: user.id },
|
|
slug: parsedAppSlug,
|
|
key: {},
|
|
teamId: undefined,
|
|
});
|
|
return newCredential.id;
|
|
} catch (error) {
|
|
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
|
|
const existing = await prisma.credential.findFirst({
|
|
where: { appId: parsedAppSlug, userId: user.id, teamId: null },
|
|
select: { id: true },
|
|
});
|
|
return existing?.id ?? null;
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getEventTypeGroupsForStep = async (
|
|
user: MinimalUser,
|
|
userTeams: MinimalTeam[],
|
|
parsedTeamIdParam: number | undefined,
|
|
isOrg: boolean,
|
|
isConferencing: boolean
|
|
): Promise<TEventTypeGroup[] | null> => {
|
|
let groups: TEventTypeGroup[] | null = null;
|
|
if (isOrg) {
|
|
groups = await getEventTypes({
|
|
userId: user.id,
|
|
teamIds: userTeams.map((item) => item.id),
|
|
isConferencing,
|
|
});
|
|
} else if (parsedTeamIdParam) {
|
|
groups = await getEventTypes({ userId: user.id, teamIds: [parsedTeamIdParam], isConferencing });
|
|
} else {
|
|
groups = await getEventTypes({ userId: user.id, isConferencing });
|
|
}
|
|
|
|
if (isConferencing && groups) {
|
|
const destinationCalendar = await prisma.destinationCalendar.findFirst({
|
|
where: { userId: user.id, eventTypeId: null },
|
|
});
|
|
|
|
groups.forEach((group) => {
|
|
group.eventTypes = group.eventTypes.map((eventType) => {
|
|
if (!eventType.destinationCalendar) {
|
|
return { ...eventType, destinationCalendar };
|
|
}
|
|
return eventType;
|
|
});
|
|
});
|
|
}
|
|
return groups;
|
|
};
|
|
|
|
const getInitialStep = (
|
|
parsedStepParam: string | undefined,
|
|
hasTeams: boolean,
|
|
showEventTypesStep: boolean,
|
|
parsedAppSlug: string,
|
|
isOAuth: boolean
|
|
): { step: string | undefined; redirect?: RedirectResult } => {
|
|
if (!hasTeams && parsedStepParam === AppOnboardingSteps.ACCOUNTS_STEP && showEventTypesStep && !isOAuth) {
|
|
return {
|
|
step: parsedStepParam,
|
|
redirect: {
|
|
redirect: { permanent: false, destination: `/apps/installation/event-types?slug=${parsedAppSlug}` },
|
|
},
|
|
};
|
|
}
|
|
let step = parsedStepParam;
|
|
if (!parsedStepParam) {
|
|
if (hasTeams) {
|
|
step = AppOnboardingSteps.ACCOUNTS_STEP;
|
|
} else {
|
|
step = isOAuth ? AppOnboardingSteps.ACCOUNTS_STEP : AppOnboardingSteps.EVENT_TYPES_STEP;
|
|
}
|
|
}
|
|
return { step };
|
|
};
|
|
|
|
const getAppAndMetadata = async (
|
|
parsedAppSlug: string
|
|
): Promise<{
|
|
app: { slug: string; keys: Prisma.JsonValue; enabled: boolean; dirName: string } | null;
|
|
appMetadata: import("@calcom/types/App").AppMeta | null;
|
|
redirect?: RedirectResult;
|
|
}> => {
|
|
const app = await getAppBySlug(parsedAppSlug);
|
|
if (!app)
|
|
return {
|
|
app: null,
|
|
appMetadata: null,
|
|
redirect: { redirect: { permanent: false, destination: "/apps" } },
|
|
};
|
|
const appMetadata = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata];
|
|
if (!appMetadata)
|
|
return {
|
|
app: null,
|
|
appMetadata: null,
|
|
redirect: { redirect: { permanent: false, destination: "/apps" } },
|
|
};
|
|
return { app, appMetadata };
|
|
};
|
|
|
|
const getCredentialId = (
|
|
parsedTeamIdParam: number | undefined,
|
|
appInstalls: Prisma.CredentialGetPayload<Record<string, never>>[],
|
|
userId: number
|
|
): number | null => {
|
|
if (parsedTeamIdParam) {
|
|
return appInstalls.find((item) => item.teamId === parsedTeamIdParam)?.id ?? null;
|
|
}
|
|
return appInstalls.find((item) => item.userId === userId)?.id ?? null;
|
|
};
|
|
|
|
const prepareUserTeams = async (
|
|
user: MinimalUser & { teams: MinimalTeam[] },
|
|
parsedTeamIdParam: number | undefined
|
|
): Promise<{ userTeams: MinimalTeam[]; isOrg: boolean; redirect?: RedirectResult }> => {
|
|
let userTeams = user.teams;
|
|
let isOrg = false;
|
|
if (parsedTeamIdParam) {
|
|
const currentTeam = userTeams.find((team: MinimalTeam) => team.id === parsedTeamIdParam);
|
|
if (!currentTeam?.id)
|
|
return {
|
|
userTeams: [],
|
|
isOrg: false,
|
|
redirect: { redirect: { permanent: false, destination: "/apps" } },
|
|
};
|
|
if (currentTeam.isOrganization) {
|
|
userTeams = [...userTeams, ...(await getOrgSubTeams(parsedTeamIdParam))];
|
|
isOrg = true;
|
|
}
|
|
}
|
|
return { userTeams, isOrg };
|
|
};
|
|
|
|
const getCredential = async (
|
|
parsedTeamIdParam: number | undefined,
|
|
appInstalls: Prisma.CredentialGetPayload<Record<string, never>>[],
|
|
user: MinimalUser & { teams: MinimalTeam[] },
|
|
initialStep: string,
|
|
appMetadata: import("@calcom/types/App").AppMeta,
|
|
parsedAppSlug: string
|
|
): Promise<{ credentialId: number | null; redirect?: RedirectResult }> => {
|
|
let credentialId = getCredentialId(parsedTeamIdParam, appInstalls, user.id);
|
|
if (
|
|
!credentialId &&
|
|
!user.teams.length &&
|
|
initialStep === AppOnboardingSteps.EVENT_TYPES_STEP &&
|
|
!appMetadata.isOAuth
|
|
) {
|
|
credentialId = await handleAutoInstall(user, appMetadata, parsedAppSlug);
|
|
if (!credentialId)
|
|
return { credentialId: null, redirect: { redirect: { permanent: false, destination: "/apps" } } };
|
|
}
|
|
return { credentialId };
|
|
};
|
|
|
|
const getTeamsInstalled = (
|
|
hasTeams: boolean,
|
|
userTeams: MinimalTeam[],
|
|
appInstalls: Prisma.CredentialGetPayload<Record<string, never>>[]
|
|
): import("../../../../modules/apps/installation/[[...step]]/step-view").TTeams => {
|
|
if (!hasTeams) return [];
|
|
return userTeams.map((team: MinimalTeam) => ({
|
|
...team,
|
|
alreadyInstalled: appInstalls.some((install) => !!install.teamId && install.teamId === team.id),
|
|
}));
|
|
};
|
|
|
|
const getParsedStep = (paramsStep: string | string[] | undefined): string | undefined => {
|
|
if (Array.isArray(paramsStep)) return paramsStep[0];
|
|
return paramsStep;
|
|
};
|
|
|
|
const getInstallationParams = async (
|
|
context: GetServerSidePropsContext
|
|
): Promise<{
|
|
parsedAppSlug: string;
|
|
parsedStepParam: string | undefined;
|
|
parsedTeamIdParam: number | undefined;
|
|
sessionId: number | null;
|
|
}> => {
|
|
const { query, params, req } = context;
|
|
const parsedAppSlug = z.coerce.string().parse(query?.slug);
|
|
const parsedStepParam = z.string().optional().parse(getParsedStep(params?.step));
|
|
const parsedTeamIdParam = z.coerce.number().optional().parse(query?.teamId);
|
|
const session = await getServerSession({ req });
|
|
const sessionId = (session?.user as { id?: number })?.id ?? null;
|
|
return { parsedAppSlug, parsedStepParam, parsedTeamIdParam, sessionId };
|
|
};
|
|
|
|
export const getServerSideProps = async (
|
|
context: GetServerSidePropsContext
|
|
): Promise<GetServerSidePropsResult<OnboardingPageProps>> => {
|
|
const { parsedAppSlug, parsedStepParam, parsedTeamIdParam, sessionId } =
|
|
await getInstallationParams(context);
|
|
if (!sessionId) return { redirect: { permanent: false, destination: "/auth/login" } };
|
|
|
|
const { app, appMetadata, redirect: appRedirect } = await getAppAndMetadata(parsedAppSlug);
|
|
if (appRedirect) return appRedirect;
|
|
|
|
const user = await getUser(sessionId);
|
|
if (!user?.username || !appMetadata) return { redirect: { permanent: false, destination: "/apps" } };
|
|
const appMetadataFinal = appMetadata;
|
|
|
|
const showEventTypesStep =
|
|
appMetadataFinal.extendsFeature === "EventType" || isConferencingApp(appMetadataFinal.categories);
|
|
const { userTeams, isOrg, redirect: teamRedirect } = await prepareUserTeams(user, parsedTeamIdParam);
|
|
if (teamRedirect) return teamRedirect;
|
|
const { step: initialStep, redirect: stepRedirect } = getInitialStep(
|
|
parsedStepParam,
|
|
!!userTeams.length,
|
|
showEventTypesStep,
|
|
parsedAppSlug,
|
|
!!appMetadataFinal.isOAuth
|
|
);
|
|
if (stepRedirect) return stepRedirect;
|
|
if (!initialStep) {
|
|
return { redirect: { permanent: false, destination: "/apps" } };
|
|
}
|
|
|
|
let initialStepValidated: (typeof STEPS)[number];
|
|
try {
|
|
initialStepValidated = z.enum(STEPS).parse(initialStep);
|
|
} catch {
|
|
return { redirect: { permanent: false, destination: "/apps" } };
|
|
}
|
|
|
|
const appInstalls = await getAppInstallsBySlug(
|
|
parsedAppSlug,
|
|
user.id,
|
|
userTeams.map(({ id }: MinimalTeam) => id)
|
|
);
|
|
const { credentialId, redirect: credRedirect } = await getCredential(
|
|
parsedTeamIdParam,
|
|
appInstalls,
|
|
user,
|
|
initialStepValidated,
|
|
appMetadataFinal,
|
|
parsedAppSlug
|
|
);
|
|
if (credRedirect) return credRedirect;
|
|
|
|
let eventTypeGroups = null;
|
|
if (initialStepValidated === AppOnboardingSteps.EVENT_TYPES_STEP) {
|
|
if (!showEventTypesStep) {
|
|
return {
|
|
redirect: {
|
|
permanent: false,
|
|
destination: `/apps/installed/${appMetadataFinal.categories[0]}?hl=${appMetadataFinal.slug}`,
|
|
},
|
|
};
|
|
}
|
|
eventTypeGroups = await getEventTypeGroupsForStep(
|
|
user,
|
|
userTeams,
|
|
parsedTeamIdParam,
|
|
isOrg,
|
|
isConferencingApp(appMetadataFinal.categories)
|
|
);
|
|
if (!credentialId) return { redirect: { permanent: false, destination: "/apps" } };
|
|
}
|
|
|
|
const personalAccount = {
|
|
id: user.id,
|
|
name: user.name,
|
|
avatarUrl: user.avatarUrl,
|
|
alreadyInstalled: appInstalls.some((install) => !install.teamId && install.userId === user.id),
|
|
};
|
|
const teams = getTeamsInstalled(!!user.teams.length, userTeams, appInstalls);
|
|
|
|
return {
|
|
props: {
|
|
app,
|
|
appMetadata: appMetadataFinal,
|
|
showEventTypesStep,
|
|
step: initialStepValidated,
|
|
personalAccount,
|
|
teams,
|
|
eventTypeGroups,
|
|
userName: user.username,
|
|
credentialId: credentialId || undefined,
|
|
isConferencing: isConferencingApp(appMetadataFinal.categories),
|
|
isOrg,
|
|
installableOnTeams:
|
|
!!appMetadataFinal?.concurrentMeetings || !isConferencingApp(appMetadataFinal.categories),
|
|
} as OnboardingPageProps,
|
|
};
|
|
};
|