Skip to content

Commit 56e5ecf

Browse files
authored
feat: add instant meeting expiry input (#15555)
* feat: add instant meeting expiry input * fix: type err * chore: remove type * chore: update label * chore: update column name
1 parent 477665f commit 56e5ecf

File tree

15 files changed

+116
-61
lines changed

15 files changed

+116
-61
lines changed

apps/web/components/eventtype/InstantEventController.tsx

+39-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Webhook } from "@prisma/client";
22
import { useSession } from "next-auth/react";
33
import type { EventTypeSetup } from "pages/event-types/[type]";
44
import { useState } from "react";
5-
import { useFormContext } from "react-hook-form";
5+
import { useFormContext, Controller } from "react-hook-form";
66

77
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
88
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
@@ -15,7 +15,17 @@ import { classNames } from "@calcom/lib";
1515
import { useLocale } from "@calcom/lib/hooks/useLocale";
1616
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
1717
import { trpc } from "@calcom/trpc/react";
18-
import { Alert, Button, EmptyScreen, SettingsToggle, Dialog, DialogContent, showToast } from "@calcom/ui";
18+
import {
19+
Alert,
20+
Button,
21+
EmptyScreen,
22+
SettingsToggle,
23+
Dialog,
24+
DialogContent,
25+
showToast,
26+
TextField,
27+
Label,
28+
} from "@calcom/ui";
1929

2030
type InstantEventControllerProps = {
2131
eventType: EventTypeSetup;
@@ -85,7 +95,33 @@ export default function InstantEventController({
8595
}
8696
}}>
8797
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
88-
{instantEventState && <InstantMeetingWebhooks eventType={eventType} />}
98+
{instantEventState && (
99+
<div className="flex flex-col gap-2">
100+
<Controller
101+
name="instantMeetingExpiryTimeOffsetInSeconds"
102+
render={({ field: { value, onChange } }) => (
103+
<>
104+
<Label>{t("set_instant_meeting_expiry_time_offset_description")}</Label>
105+
<TextField
106+
required
107+
name="instantMeetingExpiryTimeOffsetInSeconds"
108+
labelSrOnly
109+
type="number"
110+
defaultValue={value}
111+
min={10}
112+
containerClassName="max-w-80"
113+
addOnSuffix={<>{t("seconds")}</>}
114+
onChange={(e) => {
115+
onChange(Math.abs(Number(e.target.value)));
116+
}}
117+
data-testid="instant-meeting-expiry-time-offset"
118+
/>
119+
</>
120+
)}
121+
/>
122+
<InstantMeetingWebhooks eventType={eventType} />
123+
</div>
124+
)}
89125
</div>
90126
</SettingsToggle>
91127
</>
@@ -213,9 +249,6 @@ const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) =>
213249
</>
214250
) : (
215251
<>
216-
<p className="text-default mb-4 text-sm font-normal">
217-
{t("warning_payment_instant_meeting_event")}
218-
</p>
219252
<EmptyScreen
220253
Icon="webhook"
221254
headline={t("create_your_first_webhook")}

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

+1
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
251251
destinationCalendar: eventType.destinationCalendar,
252252
recurringEvent: eventType.recurringEvent || null,
253253
isInstantEvent: eventType.isInstantEvent,
254+
instantMeetingExpiryTimeOffsetInSeconds: eventType.instantMeetingExpiryTimeOffsetInSeconds,
254255
description: eventType.description ?? undefined,
255256
schedule: eventType.schedule || undefined,
256257
bookingLimits: eventType.bookingLimits || undefined,

apps/web/public/static/locales/en/common.json

+1
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,7 @@
10431043
"seats_nearly_full": "Seats almost full",
10441044
"seats_half_full": "Seats filling fast",
10451045
"number_of_seats": "Number of seats per booking",
1046+
"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.",
10461047
"enter_number_of_seats": "Enter number of seats",
10471048
"you_can_manage_your_schedules": "You can manage your schedules on the Availability page.",
10481049
"booking_full": "No more seats available",

packages/features/bookings/Booker/components/hooks/useBookings.ts

-5
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,6 @@ export const useBookings = ({ event, hashedLink, bookingForm, metadata, teamMemb
204204
});
205205
},
206206
onError: (err, _, ctx) => {
207-
// TODO:
208-
// const vercelId = ctx?.meta?.headers?.get("x-vercel-id");
209-
// if (vercelId) {
210-
// setResponseVercelIdHeader(vercelId);
211-
// }
212207
bookerFormErrorRef && bookerFormErrorRef.current?.scrollIntoView({ behavior: "smooth" });
213208
},
214209
});

packages/features/eventtypes/lib/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export type FormValues = {
3434
eventName: string;
3535
slug: string;
3636
isInstantEvent: boolean;
37+
instantMeetingExpiryTimeOffsetInSeconds: number;
3738
length: number;
3839
offsetStart: number;
3940
description: string;

packages/features/instant-meeting/handleInstantMeeting.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,26 @@ async function handler(req: NextApiRequest) {
194194
const newBooking = await prisma.booking.create(createBookingObj);
195195

196196
// Create Instant Meeting Token
197+
197198
const token = randomBytes(32).toString("hex");
199+
200+
const eventTypeWithExpiryTimeOffset = await prisma.eventType.findUniqueOrThrow({
201+
where: {
202+
id: req.body.eventTypeId,
203+
},
204+
select: {
205+
instantMeetingExpiryTimeOffsetInSeconds: true,
206+
},
207+
});
208+
209+
const instantMeetingExpiryTimeOffsetInSeconds =
210+
eventTypeWithExpiryTimeOffset?.instantMeetingExpiryTimeOffsetInSeconds ?? 90;
211+
198212
const instantMeetingToken = await prisma.instantMeetingToken.create({
199213
data: {
200214
token,
201-
// 90 Seconds
202-
expires: new Date(new Date().getTime() + 1000 * 90),
215+
// current time + offset Seconds
216+
expires: new Date(new Date().getTime() + 1000 * instantMeetingExpiryTimeOffsetInSeconds),
203217
team: {
204218
connect: {
205219
id: eventType.team.id,

packages/lib/event-types/getEventTypeById.ts

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export const getEventTypeById = async ({
8484
description: true,
8585
length: true,
8686
isInstantEvent: true,
87+
instantMeetingExpiryTimeOffsetInSeconds: true,
8788
aiPhoneCallConfig: true,
8889
offsetStart: true,
8990
hidden: true,

packages/lib/server/eventTypeSelect.ts

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const eventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
4040
slotInterval: true,
4141
successRedirectUrl: true,
4242
isInstantEvent: true,
43+
instantMeetingExpiryTimeOffsetInSeconds: true,
4344
aiPhoneCallConfig: true,
4445
assignAllTeamMembers: true,
4546
recurringEvent: true,

packages/lib/test/builder.ts

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
8282
description: faker.lorem.paragraph(),
8383
position: 1,
8484
isInstantEvent: false,
85+
instantMeetingExpiryTimeOffsetInSeconds: 90,
8586
locations: null,
8687
length: 15,
8788
offsetStart: 0,

packages/platform/sdk/src/endpoints/events/event-types/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export type EventType = {
5959
bookingLimits: number | null;
6060
durationLimits: number | null;
6161
isInstantEvent: boolean;
62+
instantMeetingExpiryTimeOffsetInSeconds: number;
6263
assignAllTeamMembers: boolean;
6364
useEventTypeDestinationCalendarEmail: boolean;
6465
};

packages/platform/sdk/src/endpoints/events/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export type Event = {
4545
eventName: string;
4646
slug: string;
4747
isInstantEvent: boolean;
48+
instantMeetingExpiryTimeOffsetInSeconds: number;
4849
aiPhoneCallConfig: {
4950
eventTypeId: number;
5051
enabled: boolean;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "EventType" ADD COLUMN "instantMeetingExpiryTimeOffsetInSeconds" INTEGER NOT NULL DEFAULT 90;

packages/prisma/schema.prisma

+49-48
Original file line numberDiff line numberDiff line change
@@ -77,65 +77,66 @@ model EventType {
7777
profileId Int?
7878
profile Profile? @relation(fields: [profileId], references: [id], onDelete: Cascade)
7979
80-
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
81-
teamId Int?
82-
hashedLink HashedLink?
83-
bookings Booking[]
84-
availability Availability[]
85-
webhooks Webhook[]
86-
destinationCalendar DestinationCalendar?
87-
eventName String?
88-
customInputs EventTypeCustomInput[]
89-
parentId Int?
90-
parent EventType? @relation("managed_eventtype", fields: [parentId], references: [id], onDelete: Cascade)
91-
children EventType[] @relation("managed_eventtype")
80+
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
81+
teamId Int?
82+
hashedLink HashedLink?
83+
bookings Booking[]
84+
availability Availability[]
85+
webhooks Webhook[]
86+
destinationCalendar DestinationCalendar?
87+
eventName String?
88+
customInputs EventTypeCustomInput[]
89+
parentId Int?
90+
parent EventType? @relation("managed_eventtype", fields: [parentId], references: [id], onDelete: Cascade)
91+
children EventType[] @relation("managed_eventtype")
9292
/// @zod.custom(imports.eventTypeBookingFields)
93-
bookingFields Json?
94-
timeZone String?
95-
periodType PeriodType @default(UNLIMITED)
93+
bookingFields Json?
94+
timeZone String?
95+
periodType PeriodType @default(UNLIMITED)
9696
/// @zod.custom(imports.coerceToDate)
97-
periodStartDate DateTime?
97+
periodStartDate DateTime?
9898
/// @zod.custom(imports.coerceToDate)
99-
periodEndDate DateTime?
100-
periodDays Int?
101-
periodCountCalendarDays Boolean?
102-
lockTimeZoneToggleOnBookingPage Boolean @default(false)
103-
requiresConfirmation Boolean @default(false)
104-
requiresBookerEmailVerification Boolean @default(false)
99+
periodEndDate DateTime?
100+
periodDays Int?
101+
periodCountCalendarDays Boolean?
102+
lockTimeZoneToggleOnBookingPage Boolean @default(false)
103+
requiresConfirmation Boolean @default(false)
104+
requiresBookerEmailVerification Boolean @default(false)
105105
/// @zod.custom(imports.recurringEventType)
106-
recurringEvent Json?
107-
disableGuests Boolean @default(false)
108-
hideCalendarNotes Boolean @default(false)
106+
recurringEvent Json?
107+
disableGuests Boolean @default(false)
108+
hideCalendarNotes Boolean @default(false)
109109
/// @zod.min(0)
110-
minimumBookingNotice Int @default(120)
111-
beforeEventBuffer Int @default(0)
112-
afterEventBuffer Int @default(0)
113-
seatsPerTimeSlot Int?
114-
onlyShowFirstAvailableSlot Boolean @default(false)
115-
seatsShowAttendees Boolean? @default(false)
116-
seatsShowAvailabilityCount Boolean? @default(true)
117-
schedulingType SchedulingType?
118-
schedule Schedule? @relation(fields: [scheduleId], references: [id])
119-
scheduleId Int?
110+
minimumBookingNotice Int @default(120)
111+
beforeEventBuffer Int @default(0)
112+
afterEventBuffer Int @default(0)
113+
seatsPerTimeSlot Int?
114+
onlyShowFirstAvailableSlot Boolean @default(false)
115+
seatsShowAttendees Boolean? @default(false)
116+
seatsShowAvailabilityCount Boolean? @default(true)
117+
schedulingType SchedulingType?
118+
schedule Schedule? @relation(fields: [scheduleId], references: [id])
119+
scheduleId Int?
120120
// price is deprecated. It has now moved to metadata.apps.stripe.price. Plan to drop this column.
121-
price Int @default(0)
121+
price Int @default(0)
122122
// currency is deprecated. It has now moved to metadata.apps.stripe.currency. Plan to drop this column.
123-
currency String @default("usd")
124-
slotInterval Int?
123+
currency String @default("usd")
124+
slotInterval Int?
125125
/// @zod.custom(imports.EventTypeMetaDataSchema)
126-
metadata Json?
126+
metadata Json?
127127
/// @zod.custom(imports.successRedirectUrl)
128-
successRedirectUrl String?
129-
forwardParamsSuccessRedirect Boolean? @default(true)
130-
workflows WorkflowsOnEventTypes[]
128+
successRedirectUrl String?
129+
forwardParamsSuccessRedirect Boolean? @default(true)
130+
workflows WorkflowsOnEventTypes[]
131131
/// @zod.custom(imports.intervalLimitsType)
132-
bookingLimits Json?
132+
bookingLimits Json?
133133
/// @zod.custom(imports.intervalLimitsType)
134-
durationLimits Json?
135-
isInstantEvent Boolean @default(false)
136-
assignAllTeamMembers Boolean @default(false)
137-
useEventTypeDestinationCalendarEmail Boolean @default(false)
138-
aiPhoneCallConfig AIPhoneCallConfiguration?
134+
durationLimits Json?
135+
isInstantEvent Boolean @default(false)
136+
instantMeetingExpiryTimeOffsetInSeconds Int @default(90)
137+
assignAllTeamMembers Boolean @default(false)
138+
useEventTypeDestinationCalendarEmail Boolean @default(false)
139+
aiPhoneCallConfig AIPhoneCallConfiguration?
139140
140141
secondaryEmailId Int?
141142
secondaryEmail SecondaryEmail? @relation(fields: [secondaryEmailId], references: [id], onDelete: Cascade)

packages/prisma/zod-utils.ts

+1
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,7 @@ export const allManagedEventTypeProps: { [k in keyof Omit<Prisma.EventTypeSelect
608608
title: true,
609609
description: true,
610610
isInstantEvent: true,
611+
instantMeetingExpiryTimeOffsetInSeconds: true,
611612
aiPhoneCallConfig: true,
612613
currency: true,
613614
periodDays: true,

packages/trpc/server/routers/viewer/eventTypes/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const EventTypeUpdateInput = _EventTypeModel
88
/** Optional fields */
99
.extend({
1010
isInstantEvent: z.boolean().optional(),
11+
instantMeetingExpiryTimeOffsetInSeconds: z.number().optional(),
1112
aiPhoneCallConfig: z
1213
.object({
1314
generalPrompt: z.string(),

0 commit comments

Comments
 (0)