Skip to content

Commit

Permalink
feat: new org onboarding wizard (#13139)
Browse files Browse the repository at this point in the history
* added layouts for moving teams step

* added admin section for org creation step

* extended org creation to also move existing teams

* wip

* wip

* wip

* further changes

* Add checkout for org in onboarding

* Fix ts errors

* Self review feedback

* Self review addressed

* Fix unit tests

* Fix ts error

* Seans feedback addressed

* feat: fix correct accounts pending

* fix: unit tests for new invite member permissions

* tests: org admin onboarding tests for existing user

* tests: Inital user self serve flow

* chore: update teamAndUserFixture to create X amount of teams

* chore: add testId to card actionButton

* test: add test-Id to continue or checkout button

* tests: add tests for migrating existing teams

* feat: match new designs

* fix: isAdminCheck

* chore: fix pricing copy

* fix: flacky tests

* Fix tests?

* More test fixes

* Check all checkboxes

* Fix type error

* Fix failing test and typescript issues

* Fix unpaid org allowing auto-add users

* Add self-serve flag

* Skip tests

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
  • Loading branch information
7 people committed Apr 3, 2024
1 parent c3b1de4 commit b3f4cfa
Show file tree
Hide file tree
Showing 49 changed files with 1,809 additions and 751 deletions.
4 changes: 3 additions & 1 deletion apps/api/v1/pages/api/teams/_post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { NextApiRequest } from "next";

import { getStripeCustomerIdFromUserId } from "@calcom/app-store/stripepayment/lib/customer";
import stripe from "@calcom/app-store/stripepayment/lib/server";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
Expand Down Expand Up @@ -197,8 +198,9 @@ const generateTeamCheckoutSession = async ({
customer_update: {
address: "auto",
},
// Disabled when testing locally as usually developer doesn't setup Tax in Stripe Test mode
automatic_tax: {
enabled: true,
enabled: IS_PRODUCTION,
},
metadata: {
pendingPaymentTeamId,
Expand Down

This file was deleted.

19 changes: 2 additions & 17 deletions apps/web/pages/settings/organizations/[id]/add-teams.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"use client";

import { redirect, useRouter } from "next/navigation";

import { AddNewTeamsForm } from "@calcom/features/ee/organizations/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Meta, WizardLayout } from "@calcom/ui";
Expand All @@ -22,16 +20,8 @@ const AddNewTeamsPage = () => {
};

AddNewTeamsPage.getLayout = (page: React.ReactElement) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const router = useRouter();

return (
<WizardLayout
currentStep={5}
maxSteps={5}
isOptionalCallback={() => {
router.push(`/event-types`);
}}>
<WizardLayout currentStep={5} maxSteps={5}>
{page}
</WizardLayout>
);
Expand All @@ -41,12 +31,7 @@ AddNewTeamsPage.PageWrapper = PageWrapper;

export const WrapperAddNewTeamsPage = (page: React.ReactElement) => {
return (
<WizardLayoutAppDir
currentStep={5}
maxSteps={5}
isOptionalCallback={() => {
redirect(`/event-types`);
}}>
<WizardLayoutAppDir currentStep={5} maxSteps={5}>
{page}
</WizardLayoutAppDir>
);
Expand Down
39 changes: 0 additions & 39 deletions apps/web/pages/settings/organizations/[id]/set-password.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion apps/web/pages/settings/organizations/new/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const CreateNewOrganizationPage = ({ querySlug }: inferSSRProps<typeof getServer
return (
<LicenseRequired>
<Meta title={t("set_up_your_organization")} description={t("organizations_description")} />
<CreateANewOrganizationForm slug={querySlug} />
<CreateANewOrganizationForm />
</LicenseRequired>
);
};
Expand Down
202 changes: 107 additions & 95 deletions apps/web/playwright/fixtures/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,22 @@ const createTeamAndAddUser = async (
isOrgVerified,
hasSubteam,
organizationId,
isDnsSetup,
index,
}: {
user: { id: number; email: string; username: string | null; role?: MembershipRole };
isUnpublished?: boolean;
isOrg?: boolean;
isOrgVerified?: boolean;
isDnsSetup?: boolean;
hasSubteam?: true;
organizationId?: number | null;
index?: number;
},
workerInfo: WorkerInfo
) => {
const slug = `${isOrg ? "org" : "team"}-${workerInfo.workerIndex}-${Date.now()}`;
const slugIndex = index ? `-count-${index}` : "";
const slug = `${isOrg ? "org" : "team"}-${workerInfo.workerIndex}-${Date.now()}${slugIndex}`;
const data: PrismaType.TeamCreateInput = {
name: `user-id-${user.id}'s ${isOrg ? "Org" : "Team"}`,
isOrganization: isOrg,
Expand All @@ -128,9 +133,9 @@ const createTeamAndAddUser = async (
if (isOrg) {
data.organizationSettings = {
create: {
isOrganizationVerified: !!isOrgVerified,
orgAutoAcceptEmail: user.email.split("@")[1],
isOrganizationConfigured: false,
isOrganizationVerified: !!isOrgVerified,
isOrganizationConfigured: isDnsSetup,
},
};
}
Expand Down Expand Up @@ -211,6 +216,7 @@ export const createUsersFixture = (
scenario: {
seedRoutingForms?: boolean;
hasTeam?: true;
numberOfTeams?: number;
teamRole?: MembershipRole;
teammates?: CustomUserOpts[];
schedulingType?: SchedulingType;
Expand All @@ -219,6 +225,7 @@ export const createUsersFixture = (
teamEventLength?: number;
isOrg?: boolean;
isOrgVerified?: boolean;
isDnsSetup?: boolean;
hasSubteam?: true;
isUnpublished?: true;
} = {}
Expand Down Expand Up @@ -382,103 +389,108 @@ export const createUsersFixture = (
include: userIncludes,
});
if (scenario.hasTeam) {
const team = await createTeamAndAddUser(
{
user: {
id: user.id,
email: user.email,
username: user.username,
role: scenario.teamRole || "OWNER",
},
isUnpublished: scenario.isUnpublished,
isOrg: scenario.isOrg,
isOrgVerified: scenario.isOrgVerified,
hasSubteam: scenario.hasSubteam,
organizationId: opts?.organizationId,
},
workerInfo
);
store.teams.push(team);
const teamEvent = await createTeamEventType(user, team, scenario);
if (scenario.teammates) {
// Create Teammate users
const teamMates = [];
for (const teammateObj of scenario.teammates) {
const teamUser = await prisma.user.create({
data: createUser(workerInfo, teammateObj),
});

// Add teammates to the team
await prisma.membership.create({
data: {
teamId: team.id,
userId: teamUser.id,
role: MembershipRole.MEMBER,
accepted: true,
},
});

// Add teammate to the host list of team event
await prisma.host.create({
data: {
userId: teamUser.id,
eventTypeId: teamEvent.id,
isFixed: scenario.schedulingType === SchedulingType.COLLECTIVE ? true : false,
const numberOfTeams = scenario.numberOfTeams || 1;
for (let i = 0; i < numberOfTeams; i++) {
const team = await createTeamAndAddUser(
{
user: {
id: user.id,
email: user.email,
username: user.username,
role: scenario.teamRole || "OWNER",
},
});

const teammateFixture = createUserFixture(
await prisma.user.findUniqueOrThrow({
where: { id: teamUser.id },
include: userIncludes,
}),
store.page
);
teamMates.push(teamUser);
store.users.push(teammateFixture);
}
// Add Teammates to OrgUsers
if (scenario.isOrg) {
const orgProfilesCreate = teamMates
.map((teamUser) => ({
user: {
connect: {
id: teamUser.id,
},
isUnpublished: scenario.isUnpublished,
isOrg: scenario.isOrg,
isOrgVerified: scenario.isOrgVerified,
isDnsSetup: scenario.isDnsSetup,
hasSubteam: scenario.hasSubteam,
organizationId: opts?.organizationId,
},
workerInfo
);
store.teams.push(team);
const teamEvent = await createTeamEventType(user, team, scenario);
if (scenario.teammates) {
// Create Teammate users
const teamMates = [];
for (const teammateObj of scenario.teammates) {
const teamUser = await prisma.user.create({
data: createUser(workerInfo, teammateObj),
});

// Add teammates to the team
await prisma.membership.create({
data: {
teamId: team.id,
userId: teamUser.id,
role: MembershipRole.MEMBER,
accepted: true,
},
uid: v4(),
username: teamUser.username || teamUser.email.split("@")[0],
}))
.concat([
{
user: { connect: { id: user.id } },
uid: v4(),
username: user.username || user.email.split("@")[0],
});

// Add teammate to the host list of team event
await prisma.host.create({
data: {
userId: teamUser.id,
eventTypeId: teamEvent.id,
isFixed: scenario.schedulingType === SchedulingType.COLLECTIVE ? true : false,
},
]);
});

const teammateFixture = createUserFixture(
await prisma.user.findUniqueOrThrow({
where: { id: teamUser.id },
include: userIncludes,
}),
store.page
);
teamMates.push(teamUser);
store.users.push(teammateFixture);
}
// Add Teammates to OrgUsers
if (scenario.isOrg) {
const orgProfilesCreate = teamMates
.map((teamUser) => ({
user: {
connect: {
id: teamUser.id,
},
},
uid: v4(),
username: teamUser.username || teamUser.email.split("@")[0],
}))
.concat([
{
user: { connect: { id: user.id } },
uid: v4(),
username: user.username || user.email.split("@")[0],
},
]);

const existingProfiles = await prisma.profile.findMany({
where: {
userId: _user.id,
},
});
const existingProfiles = await prisma.profile.findMany({
where: {
userId: _user.id,
},
});

await prisma.team.update({
where: {
id: team.id,
},
data: {
orgProfiles: _user.profiles.length
? {
connect: _user.profiles.map((profile) => ({ id: profile.id })),
}
: {
create: orgProfilesCreate.filter(
(profile) => !existingProfiles.map((p) => p.userId).includes(profile.user.connect.id)
),
},
},
});
await prisma.team.update({
where: {
id: team.id,
},
data: {
orgProfiles: _user.profiles.length
? {
connect: _user.profiles.map((profile) => ({ id: profile.id })),
}
: {
create: orgProfilesCreate.filter(
(profile) =>
!existingProfiles.map((p) => p.userId).includes(profile.user.connect.id)
),
},
},
});
}
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions apps/web/playwright/lib/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,8 @@ export async function fillStripeTestCheckout(page: Page) {
await page.fill("[name=cardExpiry]", "12/30");
await page.fill("[name=cardCvc]", "111");
await page.fill("[name=billingName]", "Stripe Stripeson");
await page.selectOption("[name=billingCountry]", "US");
await page.fill("[name=billingPostalCode]", "12345");
await page.click(".SubmitButton--complete-Shimmer");
}

Expand Down
Loading

0 comments on commit b3f4cfa

Please sign in to comment.