Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add instant meeting expiry input #15555

Merged
merged 7 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 39 additions & 6 deletions apps/web/components/eventtype/InstantEventController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Webhook } from "@prisma/client";
import { useSession } from "next-auth/react";
import type { EventTypeSetup } from "pages/event-types/[type]";
import { useState } from "react";
import { useFormContext } from "react-hook-form";
import { useFormContext, Controller } from "react-hook-form";

import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
Expand All @@ -15,7 +15,17 @@ import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import { Alert, Button, EmptyScreen, SettingsToggle, Dialog, DialogContent, showToast } from "@calcom/ui";
import {
Alert,
Button,
EmptyScreen,
SettingsToggle,
Dialog,
DialogContent,
showToast,
TextField,
Label,
} from "@calcom/ui";

type InstantEventControllerProps = {
eventType: EventTypeSetup;
Expand Down Expand Up @@ -85,7 +95,33 @@ export default function InstantEventController({
}
}}>
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
{instantEventState && <InstantMeetingWebhooks eventType={eventType} />}
{instantEventState && (
<div className="flex flex-col gap-2">
<Controller
name="instantMeetingExpiryTimeOffsetInSeconds"
render={({ field: { value, onChange } }) => (
<>
<Label>{t("set_instant_meeting_expiry_time_offset_description")}</Label>
<TextField
required
name="instantMeetingExpiryTimeOffsetInSeconds"
labelSrOnly
type="number"
defaultValue={value}
min={10}
containerClassName="max-w-80"
addOnSuffix={<>{t("seconds")}</>}
onChange={(e) => {
onChange(Math.abs(Number(e.target.value)));
}}
data-testid="instant-meeting-expiry-time-offset"
/>
</>
)}
/>
<InstantMeetingWebhooks eventType={eventType} />
</div>
)}
</div>
</SettingsToggle>
</>
Expand Down Expand Up @@ -213,9 +249,6 @@ const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) =>
</>
) : (
<>
<p className="text-default mb-4 text-sm font-normal">
{t("warning_payment_instant_meeting_event")}
</p>
<EmptyScreen
Icon="webhook"
headline={t("create_your_first_webhook")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@
destinationCalendar: eventType.destinationCalendar,
recurringEvent: eventType.recurringEvent || null,
isInstantEvent: eventType.isInstantEvent,
instantMeetingExpiryTimeOffsetInSeconds: eventType.instantMeetingExpiryTimeOffsetInSeconds,
description: eventType.description ?? undefined,
schedule: eventType.schedule || undefined,
bookingLimits: eventType.bookingLimits || undefined,
Expand Down Expand Up @@ -430,7 +431,7 @@
return () => {
router.events.off("routeChangeStart", handleRouteChange);
};
}, [router]);

Check warning on line 434 in apps/web/modules/event-types/views/event-types-single-view.tsx

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

apps/web/modules/event-types/views/event-types-single-view.tsx#L434

[react-hooks/exhaustive-deps] React Hook useEffect has missing dependencies: 'eventType.assignAllTeamMembers', 'eventType.children', 'eventType.hosts', 'eventType.schedulingType', and 'team'. Either include them or remove the dependency array.

const appsMetadata = formMethods.getValues("metadata")?.apps;
const availability = formMethods.watch("availability");
Expand Down
1 change: 1 addition & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,7 @@
"seats_nearly_full": "Seats almost full",
"seats_half_full": "Seats filling fast",
"number_of_seats": "Number of seats per booking",
"set_instant_meeting_expiry_time_offset_description": "Set meeting join window (seconds): The time frame in seconds within which host can join and start the meeting. After this period, the meeting join url will expire.",
"enter_number_of_seats": "Enter number of seats",
"you_can_manage_your_schedules": "You can manage your schedules on the Availability page.",
"booking_full": "No more seats available",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,6 @@ export const useBookings = ({ event, hashedLink, bookingForm, metadata, teamMemb
});
},
onError: (err, _, ctx) => {
// TODO:
// const vercelId = ctx?.meta?.headers?.get("x-vercel-id");
// if (vercelId) {
// setResponseVercelIdHeader(vercelId);
// }
bookerFormErrorRef && bookerFormErrorRef.current?.scrollIntoView({ behavior: "smooth" });
},
});
Expand Down
1 change: 1 addition & 0 deletions packages/features/eventtypes/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type FormValues = {
eventName: string;
slug: string;
isInstantEvent: boolean;
instantMeetingExpiryTimeOffsetInSeconds: number;
length: number;
offsetStart: number;
description: string;
Expand Down
18 changes: 16 additions & 2 deletions packages/features/instant-meeting/handleInstantMeeting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,26 @@ async function handler(req: NextApiRequest) {
const newBooking = await prisma.booking.create(createBookingObj);

// Create Instant Meeting Token

const token = randomBytes(32).toString("hex");

const eventTypeWithExpiryTimeOffset = await prisma.eventType.findUniqueOrThrow({
where: {
id: req.body.eventTypeId,
},
select: {
instantMeetingExpiryTimeOffsetInSeconds: true,
},
});

const instantMeetingExpiryTimeOffsetInSeconds =
eventTypeWithExpiryTimeOffset?.instantMeetingExpiryTimeOffsetInSeconds ?? 90;

const instantMeetingToken = await prisma.instantMeetingToken.create({
data: {
token,
// 90 Seconds
expires: new Date(new Date().getTime() + 1000 * 90),
// current time + offset Seconds
expires: new Date(new Date().getTime() + 1000 * instantMeetingExpiryTimeOffsetInSeconds),
team: {
connect: {
id: eventType.team.id,
Expand Down
1 change: 1 addition & 0 deletions packages/lib/event-types/getEventTypeById.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export const getEventTypeById = async ({
description: true,
length: true,
isInstantEvent: true,
instantMeetingExpiryTimeOffsetInSeconds: true,
aiPhoneCallConfig: true,
offsetStart: true,
hidden: true,
Expand Down
1 change: 1 addition & 0 deletions packages/lib/server/eventTypeSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const eventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
slotInterval: true,
successRedirectUrl: true,
isInstantEvent: true,
instantMeetingExpiryTimeOffsetInSeconds: true,
aiPhoneCallConfig: true,
assignAllTeamMembers: true,
recurringEvent: true,
Expand Down
1 change: 1 addition & 0 deletions packages/lib/test/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
description: faker.lorem.paragraph(),
position: 1,
isInstantEvent: false,
instantMeetingExpiryTimeOffsetInSeconds: 90,
locations: null,
length: 15,
offsetStart: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export type EventType = {
bookingLimits: number | null;
durationLimits: number | null;
isInstantEvent: boolean;
instantMeetingExpiryTimeOffsetInSeconds: number;
assignAllTeamMembers: boolean;
useEventTypeDestinationCalendarEmail: boolean;
};
1 change: 1 addition & 0 deletions packages/platform/sdk/src/endpoints/events/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export type Event = {
eventName: string;
slug: string;
isInstantEvent: boolean;
instantMeetingExpiryTimeOffsetInSeconds: number;
aiPhoneCallConfig: {
eventTypeId: number;
enabled: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "EventType" ADD COLUMN "instantMeetingExpiryTimeOffsetInSeconds" INTEGER NOT NULL DEFAULT 90;
97 changes: 49 additions & 48 deletions packages/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -77,65 +77,66 @@ model EventType {
profileId Int?
profile Profile? @relation(fields: [profileId], references: [id], onDelete: Cascade)

team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
teamId Int?
hashedLink HashedLink?
bookings Booking[]
availability Availability[]
webhooks Webhook[]
destinationCalendar DestinationCalendar?
eventName String?
customInputs EventTypeCustomInput[]
parentId Int?
parent EventType? @relation("managed_eventtype", fields: [parentId], references: [id], onDelete: Cascade)
children EventType[] @relation("managed_eventtype")
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
teamId Int?
hashedLink HashedLink?
bookings Booking[]
availability Availability[]
webhooks Webhook[]
destinationCalendar DestinationCalendar?
eventName String?
customInputs EventTypeCustomInput[]
parentId Int?
parent EventType? @relation("managed_eventtype", fields: [parentId], references: [id], onDelete: Cascade)
children EventType[] @relation("managed_eventtype")
/// @zod.custom(imports.eventTypeBookingFields)
bookingFields Json?
timeZone String?
periodType PeriodType @default(UNLIMITED)
bookingFields Json?
timeZone String?
periodType PeriodType @default(UNLIMITED)
/// @zod.custom(imports.coerceToDate)
periodStartDate DateTime?
periodStartDate DateTime?
/// @zod.custom(imports.coerceToDate)
periodEndDate DateTime?
periodDays Int?
periodCountCalendarDays Boolean?
lockTimeZoneToggleOnBookingPage Boolean @default(false)
requiresConfirmation Boolean @default(false)
requiresBookerEmailVerification Boolean @default(false)
periodEndDate DateTime?
periodDays Int?
periodCountCalendarDays Boolean?
lockTimeZoneToggleOnBookingPage Boolean @default(false)
requiresConfirmation Boolean @default(false)
requiresBookerEmailVerification Boolean @default(false)
/// @zod.custom(imports.recurringEventType)
recurringEvent Json?
disableGuests Boolean @default(false)
hideCalendarNotes Boolean @default(false)
recurringEvent Json?
disableGuests Boolean @default(false)
hideCalendarNotes Boolean @default(false)
/// @zod.min(0)
minimumBookingNotice Int @default(120)
beforeEventBuffer Int @default(0)
afterEventBuffer Int @default(0)
seatsPerTimeSlot Int?
onlyShowFirstAvailableSlot Boolean @default(false)
seatsShowAttendees Boolean? @default(false)
seatsShowAvailabilityCount Boolean? @default(true)
schedulingType SchedulingType?
schedule Schedule? @relation(fields: [scheduleId], references: [id])
scheduleId Int?
minimumBookingNotice Int @default(120)
beforeEventBuffer Int @default(0)
afterEventBuffer Int @default(0)
seatsPerTimeSlot Int?
onlyShowFirstAvailableSlot Boolean @default(false)
seatsShowAttendees Boolean? @default(false)
seatsShowAvailabilityCount Boolean? @default(true)
schedulingType SchedulingType?
schedule Schedule? @relation(fields: [scheduleId], references: [id])
scheduleId Int?
// price is deprecated. It has now moved to metadata.apps.stripe.price. Plan to drop this column.
price Int @default(0)
price Int @default(0)
// currency is deprecated. It has now moved to metadata.apps.stripe.currency. Plan to drop this column.
currency String @default("usd")
slotInterval Int?
currency String @default("usd")
slotInterval Int?
/// @zod.custom(imports.EventTypeMetaDataSchema)
metadata Json?
metadata Json?
/// @zod.custom(imports.successRedirectUrl)
successRedirectUrl String?
forwardParamsSuccessRedirect Boolean? @default(true)
workflows WorkflowsOnEventTypes[]
successRedirectUrl String?
forwardParamsSuccessRedirect Boolean? @default(true)
workflows WorkflowsOnEventTypes[]
/// @zod.custom(imports.intervalLimitsType)
bookingLimits Json?
bookingLimits Json?
/// @zod.custom(imports.intervalLimitsType)
durationLimits Json?
isInstantEvent Boolean @default(false)
assignAllTeamMembers Boolean @default(false)
useEventTypeDestinationCalendarEmail Boolean @default(false)
aiPhoneCallConfig AIPhoneCallConfiguration?
durationLimits Json?
isInstantEvent Boolean @default(false)
instantMeetingExpiryTimeOffsetInSeconds Int @default(90)
assignAllTeamMembers Boolean @default(false)
useEventTypeDestinationCalendarEmail Boolean @default(false)
aiPhoneCallConfig AIPhoneCallConfiguration?

secondaryEmailId Int?
secondaryEmail SecondaryEmail? @relation(fields: [secondaryEmailId], references: [id], onDelete: Cascade)
Expand Down
1 change: 1 addition & 0 deletions packages/prisma/zod-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,7 @@ export const allManagedEventTypeProps: { [k in keyof Omit<Prisma.EventTypeSelect
title: true,
description: true,
isInstantEvent: true,
instantMeetingExpiryTimeOffsetInSeconds: true,
aiPhoneCallConfig: true,
currency: true,
periodDays: true,
Expand Down
1 change: 1 addition & 0 deletions packages/trpc/server/routers/viewer/eventTypes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const EventTypeUpdateInput = _EventTypeModel
/** Optional fields */
.extend({
isInstantEvent: z.boolean().optional(),
instantMeetingExpiryTimeOffsetInSeconds: z.number().optional(),
aiPhoneCallConfig: z
.object({
generalPrompt: z.string(),
Expand Down
Loading