Deprecated API endpoints cleanup (#5478)
* Deprecated API endpoints cleanup * Linting Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
kodiakhq[bot]
parent
fcde1f7a9d
commit
abc5ff2d97
@@ -598,60 +598,6 @@ paths:
|
||||
summary: Gets and stores the OAuth token
|
||||
tags:
|
||||
- Integrations
|
||||
/api/user/profile:
|
||||
patch:
|
||||
description: Updates a user's profile.
|
||||
summary: Updates a user's profile
|
||||
tags:
|
||||
- User
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
avatar:
|
||||
type: string
|
||||
timeZone:
|
||||
type: string
|
||||
weekStart:
|
||||
type: string
|
||||
hideBranding:
|
||||
type: string
|
||||
theme:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
/api/me:
|
||||
get:
|
||||
description: Gets current user's profile.
|
||||
@@ -686,71 +632,6 @@ paths:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
/api/user/membership:
|
||||
get:
|
||||
description: Get a list of the teams the user has joined.
|
||||
summary: Get the teams a user is joined to
|
||||
tags:
|
||||
- User
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
patch:
|
||||
description: Accept team invitation
|
||||
summary: Accept team invitation.
|
||||
tags:
|
||||
- User
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
teamId:
|
||||
type: string
|
||||
delete:
|
||||
description: Leave team or decline membership invite of current user
|
||||
summary: Leave team or decline team invite.
|
||||
tags:
|
||||
- User
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
teamId:
|
||||
type: string
|
||||
"/api/{team}":
|
||||
delete:
|
||||
description: Deletes a team
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
import { ButtonBaseProps } from "@calcom/ui/Button";
|
||||
import { Dialog } from "@calcom/ui/Dialog";
|
||||
import showToast from "@calcom/ui/v2/core/notifications";
|
||||
|
||||
import DeleteStripeDialogContent from "@components/dialog/DeleteStripeDialogContent";
|
||||
|
||||
export default function DisconnectStripeIntegration(props: {
|
||||
/** Integration credential id */
|
||||
id: number;
|
||||
render: (renderProps: ButtonBaseProps) => JSX.Element;
|
||||
onOpenChange: (isOpen: boolean) => unknown | Promise<unknown>;
|
||||
}) {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const mutation = useMutation(
|
||||
async (action: string) => {
|
||||
const res = await fetch("/api/integrations", {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({ id: props.id, action }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("Something went wrong");
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
{
|
||||
async onSettled() {
|
||||
await props.onOpenChange(modalOpen);
|
||||
},
|
||||
onSuccess(data) {
|
||||
showToast(data.message, "success");
|
||||
setModalOpen(false);
|
||||
},
|
||||
}
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||
<DeleteStripeDialogContent
|
||||
variety="warning"
|
||||
title="Disconnect Stripe Integration"
|
||||
cancelAllBookingsBtnText="Cancel all bookings"
|
||||
removeBtnText="Remove payment"
|
||||
cancelBtnText="Cancel"
|
||||
onConfirm={() => {
|
||||
mutation.mutate("cancel");
|
||||
}}
|
||||
onRemove={() => {
|
||||
mutation.mutate("remove");
|
||||
}}>
|
||||
If you have unpaid and unconfirmed bookings, you must choose to cancel them or remove the required
|
||||
payment field.
|
||||
</DeleteStripeDialogContent>
|
||||
</Dialog>
|
||||
{props.render({
|
||||
onClick() {
|
||||
setModalOpen(true);
|
||||
},
|
||||
disabled: modalOpen,
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# Before adding new endpoints here
|
||||
|
||||
We're deprecating adding new endpoints in favor of creating tRPC procedures.
|
||||
|
||||
You can learn about [tRPC procedures in the docs](https://trpc.io/docs/v10/procedures).
|
||||
|
||||
You can see our current tRPC procedures in this file `packages/trpc/server/routers/_app.ts`
|
||||
@@ -1,85 +0,0 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
type CalendlyEventType = {
|
||||
name: string;
|
||||
slug: string;
|
||||
duration: number;
|
||||
description_plain: string;
|
||||
secret: boolean;
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req });
|
||||
const authenticatedUser = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: session?.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (req.method == "POST") {
|
||||
const userResult = await fetch("https://api.calendly.com/users/me", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + req.body.token,
|
||||
},
|
||||
});
|
||||
|
||||
if (userResult.status == 200) {
|
||||
const userData = await userResult.json();
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: authenticatedUser.id,
|
||||
},
|
||||
data: {
|
||||
name: userData.resource.name,
|
||||
},
|
||||
});
|
||||
|
||||
const eventTypesResult = await fetch(
|
||||
"https://api.calendly.com/event_types?user=" + userData.resource.uri,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + req.body.token,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const eventTypesData = await eventTypesResult.json();
|
||||
|
||||
eventTypesData.collection.forEach(async (eventType: CalendlyEventType) => {
|
||||
await prisma.eventType.create({
|
||||
data: {
|
||||
title: eventType.name,
|
||||
slug: eventType.slug,
|
||||
length: eventType.duration,
|
||||
description: eventType.description_plain,
|
||||
hidden: eventType.secret,
|
||||
users: {
|
||||
connect: {
|
||||
id: authenticatedUser.id,
|
||||
},
|
||||
},
|
||||
userId: authenticatedUser.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
res.status(201).end();
|
||||
} else {
|
||||
res.status(500).end();
|
||||
}
|
||||
} else {
|
||||
res.status(405).end();
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
type SavvyCalEventType = {
|
||||
name: string;
|
||||
slug: string;
|
||||
durations: [number];
|
||||
description: string;
|
||||
state: "active";
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req });
|
||||
const authenticatedUser = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: session?.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (req.method === "POST") {
|
||||
const userResult = await fetch("https://api.savvycal.com/v1/me", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + req.body.token,
|
||||
},
|
||||
});
|
||||
|
||||
if (userResult.status === 200) {
|
||||
const userData = await userResult.json();
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: authenticatedUser.id,
|
||||
},
|
||||
data: {
|
||||
name: userData.display_name,
|
||||
timeZone: userData.time_zone,
|
||||
weekStart: userData.first_day_of_week === 0 ? "Sunday" : "Monday",
|
||||
avatar: userData.avatar_url,
|
||||
},
|
||||
});
|
||||
|
||||
const eventTypesResult = await fetch("https://api.savvycal.com/v1/links?limit=100", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + req.body.token,
|
||||
},
|
||||
});
|
||||
|
||||
const eventTypesData = await eventTypesResult.json();
|
||||
|
||||
eventTypesData.entries.forEach(async (eventType: SavvyCalEventType) => {
|
||||
await prisma.eventType.create({
|
||||
data: {
|
||||
title: eventType.name,
|
||||
slug: eventType.slug,
|
||||
length: eventType.durations[0],
|
||||
description: eventType.description.replace(/<[^>]*>?/gm, ""),
|
||||
hidden: eventType.state === "active",
|
||||
users: {
|
||||
connect: {
|
||||
id: authenticatedUser.id,
|
||||
},
|
||||
},
|
||||
userId: authenticatedUser.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
res.status(201).end();
|
||||
} else {
|
||||
res.status(500).end();
|
||||
}
|
||||
} else {
|
||||
res.status(405).end();
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import { BookingStatus, Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!["GET", "DELETE"].includes(req.method || "")) {
|
||||
return res.status(405).end();
|
||||
}
|
||||
|
||||
// Check that user is authenticated
|
||||
const session = await getSession({ req });
|
||||
const userId = session?.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
res.status(401).json({ message: "You must be logged in to do this" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === "GET") {
|
||||
const credentials = await prisma.credential.findMany({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
type: true,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(200).json(credentials);
|
||||
}
|
||||
|
||||
if (req.method == "DELETE") {
|
||||
const id = req.body.id;
|
||||
const data: Prisma.UserUpdateInput = {
|
||||
credentials: {
|
||||
delete: {
|
||||
id,
|
||||
},
|
||||
},
|
||||
};
|
||||
const integration = await prisma.credential.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
/* If the user deletes a zapier integration, we delete all his api keys as well. */
|
||||
if (integration?.appId === "zapier") {
|
||||
data.apiKeys = {
|
||||
deleteMany: {
|
||||
userId,
|
||||
appId: "zapier",
|
||||
},
|
||||
};
|
||||
/* We also delete all user's zapier wehbooks */
|
||||
data.webhooks = {
|
||||
deleteMany: {
|
||||
userId,
|
||||
appId: "zapier",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data,
|
||||
});
|
||||
|
||||
if (req.body?.action === "cancel" || req.body?.action === "remove") {
|
||||
try {
|
||||
const bookingIdsWithPayments = await prisma.booking
|
||||
.findMany({
|
||||
where: {
|
||||
userId: session?.user?.id,
|
||||
paid: false,
|
||||
NOT: {
|
||||
payment: {
|
||||
every: {
|
||||
booking: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
})
|
||||
.then((bookings) => bookings.map((booking) => booking.id));
|
||||
const deletePayments = prisma.payment.deleteMany({
|
||||
where: {
|
||||
bookingId: {
|
||||
in: bookingIdsWithPayments,
|
||||
},
|
||||
success: false,
|
||||
},
|
||||
});
|
||||
|
||||
const updateBookings = prisma.booking.updateMany({
|
||||
where: {
|
||||
id: {
|
||||
in: bookingIdsWithPayments,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
status: BookingStatus.CANCELLED,
|
||||
rejectionReason: "Payment provider got removed",
|
||||
},
|
||||
});
|
||||
|
||||
const bookingReferences = await prisma.booking
|
||||
.findMany({
|
||||
where: {
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
})
|
||||
.then((bookings) => bookings.map((booking) => booking.id));
|
||||
|
||||
const deleteBookingReferences = prisma.bookingReference.deleteMany({
|
||||
where: {
|
||||
bookingId: {
|
||||
in: bookingReferences,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (req.body?.action === "cancel") {
|
||||
await prisma.$transaction([deletePayments, updateBookings, deleteBookingReferences]);
|
||||
} else {
|
||||
const updateBookings = prisma.booking.updateMany({
|
||||
where: {
|
||||
id: {
|
||||
in: bookingIdsWithPayments,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
paid: true,
|
||||
},
|
||||
});
|
||||
await prisma.$transaction([deletePayments, updateBookings]);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).json({ message: "Integration could not be deleted" });
|
||||
}
|
||||
}
|
||||
res.status(200).json({ message: "Integration deleted successfully" });
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
import { checkUsername } from "@calcom/lib/server/checkUsername";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { userMetadata as zodUserMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { intentUsername } = req.body;
|
||||
// Check that user is authenticated
|
||||
try {
|
||||
const session = await getSession({ req });
|
||||
const userId = session?.user?.id;
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
select: {
|
||||
id: true,
|
||||
metadata: true,
|
||||
},
|
||||
where: { id: userId },
|
||||
});
|
||||
const checkPremiumUsernameResult = await checkUsername(intentUsername);
|
||||
|
||||
if (userId && user) {
|
||||
const userMetadata = zodUserMetadata.parse(user.metadata);
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
metadata: {
|
||||
...userMetadata,
|
||||
intentUsername,
|
||||
isIntentPremium: checkPremiumUsernameResult.premium,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(501).send({ message: "intent-username.save.error" });
|
||||
}
|
||||
res.end();
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: getHandler }),
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { defaultAvatarSrc } from "@lib/profile";
|
||||
|
||||
/**
|
||||
* @deprecated Use TRCP's viewer.me query
|
||||
*/
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req });
|
||||
if (!session) {
|
||||
res.status(401).json({ message: "Not authenticated" });
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUniqueOrThrow({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
bio: true,
|
||||
timeZone: true,
|
||||
weekStart: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
bufferTime: true,
|
||||
theme: true,
|
||||
createdDate: true,
|
||||
hideBranding: true,
|
||||
avatar: true,
|
||||
},
|
||||
});
|
||||
|
||||
user.avatar = user.avatar || defaultAvatarSrc({ email: user.email });
|
||||
|
||||
res.status(200).json({
|
||||
user,
|
||||
});
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { getAvailabilityFromSchedule } from "@lib/availability";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req });
|
||||
const userId = session?.user?.id;
|
||||
if (!userId) {
|
||||
res.status(401).json({ message: "Not authenticated" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.body.schedule || req.body.schedule.length !== 7) {
|
||||
return res.status(400).json({ message: "Bad Request." });
|
||||
}
|
||||
|
||||
const availability = getAvailabilityFromSchedule(req.body.schedule);
|
||||
|
||||
if (req.method === "POST") {
|
||||
try {
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
availability: {
|
||||
/* We delete user availabilty */
|
||||
deleteMany: {
|
||||
userId: {
|
||||
equals: userId,
|
||||
},
|
||||
},
|
||||
/* So we can replace it */
|
||||
createMany: {
|
||||
data: availability.map((schedule) => ({
|
||||
days: schedule.days,
|
||||
startTime: schedule.startTime,
|
||||
endTime: schedule.endTime,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return res.status(200).json({
|
||||
message: "created",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.status(500).json({ message: "Unable to create schedule." });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { closeComDeleteTeam } from "@calcom/lib/sync/SyncServiceManager";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req: req });
|
||||
if (!session) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
if (!session.user?.id) {
|
||||
console.log("Received session token without a user id.");
|
||||
return res.status(500).json({ message: "Something went wrong." });
|
||||
}
|
||||
if (!req.query.team) {
|
||||
console.log("Missing team query param.");
|
||||
return res.status(500).json({ message: "Something went wrong." });
|
||||
}
|
||||
|
||||
const teamId = parseInt(req.query.team as string);
|
||||
|
||||
// GET /api/teams/{team}
|
||||
if (req.method === "GET") {
|
||||
const team = await getTeamWithMembers(teamId);
|
||||
return res.status(200).json({ team });
|
||||
}
|
||||
// DELETE /api/teams/{team}
|
||||
if (req.method === "DELETE") {
|
||||
const membership = await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!membership || membership.role !== "OWNER") {
|
||||
console.log(`User ${session.user.id} tried deleting an organization they don't own.`);
|
||||
return res.status(403).json({ message: "Forbidden." });
|
||||
}
|
||||
|
||||
await prisma.membership.delete({
|
||||
where: {
|
||||
userId_teamId: { userId: session.user.id, teamId },
|
||||
},
|
||||
});
|
||||
const deletedTeam = await prisma.team.delete({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
});
|
||||
|
||||
// Sync Services: Close.com
|
||||
closeComDeleteTeam(deletedTeam);
|
||||
|
||||
return res.status(204).send(null);
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { randomBytes } from "crypto";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { sendTeamInviteEmail } from "@calcom/emails";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { BASE_URL } from "@lib/config/constants";
|
||||
import slugify from "@lib/slugify";
|
||||
|
||||
import { getTranslation } from "@server/lib/i18n";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const t = await getTranslation(req.body.language ?? "en", "common");
|
||||
|
||||
if (req.method !== "POST") {
|
||||
return res.status(400).json({ message: "Bad request" });
|
||||
}
|
||||
|
||||
const session = await getSession({ req });
|
||||
if (!session) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: parseInt(req.query.team as string),
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
return res.status(404).json({ message: "Invalid team" });
|
||||
}
|
||||
|
||||
const reqBody = req.body as {
|
||||
usernameOrEmail: string;
|
||||
role: MembershipRole;
|
||||
sendEmailInvitation: boolean;
|
||||
};
|
||||
const { role, sendEmailInvitation } = reqBody;
|
||||
// liberal email match
|
||||
const isEmail = (str: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
|
||||
const usernameOrEmail = isEmail(reqBody.usernameOrEmail)
|
||||
? reqBody.usernameOrEmail.toLowerCase()
|
||||
: slugify(reqBody.usernameOrEmail);
|
||||
|
||||
const invitee = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [{ username: usernameOrEmail }, { email: usernameOrEmail }],
|
||||
},
|
||||
});
|
||||
|
||||
if (!invitee) {
|
||||
const email = isEmail(usernameOrEmail) ? usernameOrEmail : undefined;
|
||||
if (!email) {
|
||||
return res.status(400).json({
|
||||
message: `Invite failed because there is no corresponding user for ${usernameOrEmail}`,
|
||||
});
|
||||
}
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
teams: {
|
||||
create: {
|
||||
team: {
|
||||
connect: {
|
||||
id: parseInt(req.query.team as string),
|
||||
},
|
||||
},
|
||||
role,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const token: string = randomBytes(32).toString("hex");
|
||||
|
||||
await prisma.verificationToken.create({
|
||||
data: {
|
||||
identifier: usernameOrEmail,
|
||||
token,
|
||||
expires: new Date(new Date().setHours(168)), // +1 week
|
||||
},
|
||||
});
|
||||
|
||||
if (session?.user?.name && team?.name) {
|
||||
await sendTeamInviteEmail({
|
||||
language: t,
|
||||
from: session.user.name,
|
||||
to: usernameOrEmail,
|
||||
teamName: team.name,
|
||||
joinLink: `${BASE_URL}/auth/signup?token=${token}&callbackUrl=/settings/teams}`,
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(201).json({});
|
||||
}
|
||||
|
||||
// create provisional membership
|
||||
try {
|
||||
await prisma.membership.create({
|
||||
data: {
|
||||
teamId: parseInt(req.query.team as string),
|
||||
userId: invitee.id,
|
||||
role,
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (err.code === "P2002") {
|
||||
// unique constraint violation
|
||||
return res.status(409).json({
|
||||
message: "This user is a member of this team / has a pending invitation.",
|
||||
});
|
||||
} else {
|
||||
throw err; // rethrow
|
||||
}
|
||||
}
|
||||
|
||||
// inform user of membership by email
|
||||
if (sendEmailInvitation && session?.user?.name && team?.name) {
|
||||
await sendTeamInviteEmail({
|
||||
language: t,
|
||||
from: session.user.name,
|
||||
to: usernameOrEmail,
|
||||
teamName: team.name,
|
||||
joinLink: BASE_URL + "/settings/teams",
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({});
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { closeComDeleteTeamMembership } from "@calcom/lib/sync/SyncServiceManager";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req });
|
||||
|
||||
if (!session) {
|
||||
res.status(401).json({ message: "Not authenticated" });
|
||||
return;
|
||||
}
|
||||
|
||||
const isTeamOwner = !!(await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId: session.user?.id,
|
||||
teamId: parseInt(req.query.team as string),
|
||||
role: "OWNER",
|
||||
},
|
||||
}));
|
||||
|
||||
if (!isTeamOwner) {
|
||||
res.status(403).json({ message: "You are not authorized to manage this team" });
|
||||
return;
|
||||
}
|
||||
|
||||
// List members
|
||||
if (req.method === "GET") {
|
||||
const memberships = await prisma.membership.findMany({
|
||||
where: {
|
||||
teamId: parseInt(req.query.team as string),
|
||||
},
|
||||
});
|
||||
|
||||
let members = await prisma.user.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: memberships.map((membership) => membership.userId),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
bio: true,
|
||||
avatar: true,
|
||||
timeZone: true,
|
||||
},
|
||||
});
|
||||
|
||||
members = members.map((member) => {
|
||||
const membership = memberships.find((membership) => member.id === membership.userId);
|
||||
return {
|
||||
...member,
|
||||
role: membership?.accepted ? membership?.role : "INVITEE",
|
||||
};
|
||||
});
|
||||
|
||||
return res.status(200).json({ members: members });
|
||||
}
|
||||
|
||||
// Cancel a membership (invite)
|
||||
if (req.method === "DELETE") {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: req.body.userId,
|
||||
},
|
||||
});
|
||||
await prisma.membership.delete({
|
||||
where: {
|
||||
userId_teamId: { userId: req.body.userId, teamId: parseInt(req.query.team as string) },
|
||||
},
|
||||
});
|
||||
// Sync Services: Close.com
|
||||
closeComDeleteTeamMembership(user);
|
||||
|
||||
return res.status(204).send(null);
|
||||
}
|
||||
|
||||
// Promote or demote a member of the team
|
||||
|
||||
res.status(200).json({});
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
// @deprecated - USE TRPC
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req });
|
||||
if (!session?.user?.id) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
const isTeamOwner = !!(await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
teamId: parseInt(req.query.team as string),
|
||||
role: "OWNER",
|
||||
},
|
||||
}));
|
||||
|
||||
if (!isTeamOwner) {
|
||||
res.status(403).json({ message: "You are not authorized to manage this team" });
|
||||
return;
|
||||
}
|
||||
|
||||
// PATCH /api/teams/profile/{team}
|
||||
if (req.method === "PATCH") {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: parseInt(req.query.team as string),
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
return res.status(404).json({ message: "Invalid team" });
|
||||
}
|
||||
|
||||
const username = req.body.username;
|
||||
const userConflict = await prisma.team.findMany({
|
||||
where: {
|
||||
slug: username,
|
||||
},
|
||||
});
|
||||
const teamId = Number(req.query.team);
|
||||
if (userConflict.some((team) => team.id !== teamId)) {
|
||||
return res.status(409).json({ message: "Team username already taken" });
|
||||
}
|
||||
|
||||
const name = req.body.name;
|
||||
const slug = req.body.username;
|
||||
const bio = req.body.description;
|
||||
const logo = req.body.logo;
|
||||
const hideBranding = req.body.hideBranding;
|
||||
|
||||
await prisma.team.update({
|
||||
where: {
|
||||
id: team.id,
|
||||
},
|
||||
data: {
|
||||
name,
|
||||
slug,
|
||||
logo,
|
||||
bio,
|
||||
hideBranding,
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).json({ message: "Team updated successfully" });
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getStripeCustomerId } from "@calcom/app-store/stripepayment/lib/customer";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { WEBSITE_URL } from "@lib/config/constants";
|
||||
import { HttpError as HttpCode } from "@lib/core/http/error";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req });
|
||||
if (!session?.user?.id) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
if (!["GET", "POST"].includes(req.method || "")) {
|
||||
throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUniqueOrThrow({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
metadata: true,
|
||||
},
|
||||
});
|
||||
|
||||
const stripeCustomerId = await getStripeCustomerId(user);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${WEBSITE_URL}/api/upgrade`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
stripeCustomerId,
|
||||
email: user.email,
|
||||
fromApp: true,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.url) throw new HttpCode({ statusCode: 401, message: data.message });
|
||||
|
||||
res.redirect(303, data.url);
|
||||
} catch (error) {
|
||||
console.error(`error`, error);
|
||||
res.redirect(303, req.headers.origin || "/");
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { pick } from "lodash";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import z from "zod";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req });
|
||||
|
||||
if (!session?.user.id) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
const querySchema = z.object({
|
||||
id: z.string().transform((val) => parseInt(val)),
|
||||
});
|
||||
|
||||
const parsedQuery = querySchema.safeParse(req.query);
|
||||
const userId = parsedQuery.success ? parsedQuery.data.id : null;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(400).json({ message: "No user id provided" });
|
||||
}
|
||||
|
||||
const authenticatedUser = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (userId !== authenticatedUser.id) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
if (req.method === "GET") {
|
||||
return res.status(405).json({ message: "Method Not Allowed" });
|
||||
}
|
||||
|
||||
if (req.method === "PATCH") {
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: {
|
||||
id: authenticatedUser.id,
|
||||
},
|
||||
data: {
|
||||
...pick(req.body.data, [
|
||||
"username",
|
||||
"name",
|
||||
"avatar",
|
||||
"timeZone",
|
||||
"timeFormat",
|
||||
"weekStart",
|
||||
"hideBranding",
|
||||
"theme",
|
||||
"completedOnboarding",
|
||||
]),
|
||||
bio: req.body.description ?? req.body.data?.bio,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
bio: true,
|
||||
avatar: true,
|
||||
timeZone: true,
|
||||
timeFormat: true,
|
||||
weekStart: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
bufferTime: true,
|
||||
hideBranding: true,
|
||||
theme: true,
|
||||
createdDate: true,
|
||||
plan: true,
|
||||
completedOnboarding: true,
|
||||
},
|
||||
});
|
||||
return res.status(200).json({ message: "User Updated", data: updatedUser });
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { deleteStripeCustomer } from "@calcom/app-store/stripepayment/lib/customer";
|
||||
import { deleteWebUser as syncServicesDeleteWebUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req });
|
||||
|
||||
if (!session?.user.id) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
if (req.method !== "DELETE") {
|
||||
return res.status(405).json({ message: "Method Not Allowed" });
|
||||
}
|
||||
|
||||
if (req.method === "DELETE") {
|
||||
// Get user
|
||||
const user = await prisma.user.findUniqueOrThrow({
|
||||
where: {
|
||||
id: session.user?.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
metadata: true,
|
||||
username: true,
|
||||
createdDate: true,
|
||||
name: true,
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
// Delete from stripe
|
||||
await deleteStripeCustomer(user).catch(console.warn);
|
||||
// Delete from Cal
|
||||
await prisma.user.delete({
|
||||
where: {
|
||||
id: session?.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Sync Services
|
||||
syncServicesDeleteWebUser(user);
|
||||
|
||||
return res.status(204).end();
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req: req });
|
||||
if (!session || !session.user?.id) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
if (req.method === "GET") {
|
||||
const memberships = await prisma.membership.findMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
const teams = await prisma.team.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: memberships.map((membership) => membership.teamId),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
membership: memberships.map((membership) => ({
|
||||
role: membership.accepted ? membership.role : "INVITEE",
|
||||
...teams.find((team) => team.id === membership.teamId),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
if (!req.body.teamId) {
|
||||
return res.status(400).json({ message: "Bad request" });
|
||||
}
|
||||
|
||||
// Leave team or decline membership invite of current user
|
||||
if (req.method === "DELETE") {
|
||||
await prisma.membership.delete({
|
||||
where: {
|
||||
userId_teamId: { userId: session.user.id, teamId: req.body.teamId },
|
||||
},
|
||||
});
|
||||
return res.status(204).send(null);
|
||||
}
|
||||
|
||||
// Accept team invitation
|
||||
if (req.method === "PATCH") {
|
||||
await prisma.membership.update({
|
||||
where: {
|
||||
userId_teamId: { userId: session.user.id, teamId: req.body.teamId },
|
||||
},
|
||||
data: {
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(204).send(null);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||
import { pick } from "lodash";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
import { resizeBase64Image } from "@server/lib/resizeBase64Image";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req: req });
|
||||
|
||||
if (!session) {
|
||||
res.status(401).json({ message: "Not authenticated" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const avatar = req.body.avatar ? await resizeBase64Image(req.body.avatar) : undefined;
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
},
|
||||
data: {
|
||||
...pick(req.body, [
|
||||
"username",
|
||||
"name",
|
||||
"timeZone",
|
||||
"weekStart",
|
||||
"hideBranding",
|
||||
"theme",
|
||||
"completedOnboarding",
|
||||
"locale",
|
||||
]),
|
||||
avatar,
|
||||
bio: req.body.description,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof PrismaClientKnownRequestError) {
|
||||
if (e.code === "P2002") {
|
||||
return res.status(409).json({ message: "Username already taken" });
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
return res.status(200).json({ message: "Profile updated successfully" });
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { ensureSession } from "@calcom/lib/auth";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse<Response>): Promise<void> {
|
||||
// Only logged in users can opt-in/out
|
||||
await ensureSession({ req });
|
||||
|
||||
// If has the cookie, Opt-out of V2
|
||||
if ("calcom-v2-early-access" in req.cookies && req.cookies["calcom-v2-early-access"] === "1") {
|
||||
res.setHeader("Set-Cookie", `calcom-v2-early-access=0; Max-Age=0; Path=/`);
|
||||
} else {
|
||||
/* Opt-int to V2 */
|
||||
res.setHeader("Set-Cookie", "calcom-v2-early-access=1; Path=/");
|
||||
}
|
||||
|
||||
let redirectUrl = "/";
|
||||
|
||||
// We take you back where you came from if possible
|
||||
if (typeof req.headers["referer"] === "string") redirectUrl = req.headers["referer"];
|
||||
|
||||
res.redirect(redirectUrl);
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(handler) }),
|
||||
});
|
||||
@@ -52,16 +52,6 @@ const BillingView = () => {
|
||||
<>
|
||||
<Meta title={t("billing")} description={t("manage_billing_description")} />
|
||||
<div className="space-y-6 text-sm sm:space-y-8">
|
||||
{!isPro && (
|
||||
<CtaRow title={t("billing_freeplan_title")} description={t("billing_freeplan_description")}>
|
||||
<form target="_blank" method="POST" action="/api/upgrade">
|
||||
<Button type="submit" EndIcon={Icon.FiExternalLink}>
|
||||
{t("billing_freeplan_cta")}
|
||||
</Button>
|
||||
</form>
|
||||
</CtaRow>
|
||||
)}
|
||||
|
||||
<CtaRow
|
||||
className={classNames(!isPro && "pointer-events-none opacity-30")}
|
||||
title={t("billing_manage_details_title")}
|
||||
|
||||
@@ -56,17 +56,8 @@ export default class LarkCalendarService implements Calendar {
|
||||
const refreshExpireDate = larkAuthCredentials.refresh_expires_date;
|
||||
const refreshToken = larkAuthCredentials.refresh_token;
|
||||
if (isExpired(refreshExpireDate) || !refreshToken) {
|
||||
const res = await fetch("/api/integrations", {
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({ id: credential.id }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("disconnection wrong");
|
||||
}
|
||||
throw new Error("refresh token expires");
|
||||
await prisma.credential.delete({ where: { id: credential.id } });
|
||||
throw new Error("Lark Calendar refresh token expired");
|
||||
}
|
||||
try {
|
||||
const appAccessToken = await getAppAccessToken();
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { TRIAL_LIMIT_DAYS } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import Button from "@calcom/ui/Button";
|
||||
|
||||
const TrialBanner = () => {
|
||||
const { t } = useLocale();
|
||||
const query = useMeQuery();
|
||||
const user = query.data;
|
||||
|
||||
if (!user || user.plan !== "TRIAL") return null;
|
||||
|
||||
const trialDaysLeft = user.trialEndsAt
|
||||
? dayjs(user.trialEndsAt).add(1, "day").diff(dayjs(), "day")
|
||||
: dayjs(user.createdDate)
|
||||
.add(TRIAL_LIMIT_DAYS + 1, "day")
|
||||
.diff(dayjs(), "day");
|
||||
|
||||
return (
|
||||
<div
|
||||
className="m-4 hidden rounded-md bg-yellow-200 p-4 text-center text-sm font-medium text-gray-600 lg:block"
|
||||
data-testid="trial-banner">
|
||||
<div className="mb-2 text-left">
|
||||
{trialDaysLeft > 0 ? t("trial_days_left", { days: trialDaysLeft }) : t("trial_expired")}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
href="/api/upgrade"
|
||||
color="minimal"
|
||||
prefetch={false}
|
||||
className="w-full justify-center border-2 border-gray-600 hover:bg-yellow-100">
|
||||
{t("upgrade_now")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrialBanner;
|
||||
@@ -1,54 +0,0 @@
|
||||
import { Trans } from "next-i18next";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import Button from "./Button";
|
||||
import { Dialog, DialogClose, DialogContent } from "./Dialog";
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
export function UpgradeToProDialog({
|
||||
modalOpen,
|
||||
setModalOpen,
|
||||
children,
|
||||
}: {
|
||||
modalOpen: boolean;
|
||||
setModalOpen: (open: boolean) => void;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<Dialog open={modalOpen}>
|
||||
<DialogContent>
|
||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-yellow-100">
|
||||
<Icon.FiAlertTriangle className="h-6 w-6 text-yellow-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mb-4 sm:flex sm:items-start">
|
||||
<div className="mt-3 sm:mt-0 sm:text-left">
|
||||
<h3 className="font-cal text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
||||
{t("only_available_on_pro_plan")}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-3">
|
||||
<p>{children}</p>
|
||||
<p>
|
||||
<Trans i18nKey="plan_upgrade_instructions">
|
||||
You can
|
||||
<a href="/api/upgrade" className="underline">
|
||||
upgrade here
|
||||
</a>
|
||||
.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-5 gap-x-2 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<DialogClose asChild>
|
||||
<Button className="table-cell w-full text-center" onClick={() => setModalOpen(false)}>
|
||||
{t("dismiss")}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user