diff --git a/apps/web/components/eventtype/InstantEventController.tsx b/apps/web/components/eventtype/InstantEventController.tsx index a3b2d49008..83ff094983 100644 --- a/apps/web/components/eventtype/InstantEventController.tsx +++ b/apps/web/components/eventtype/InstantEventController.tsx @@ -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"; @@ -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; @@ -85,7 +95,33 @@ export default function InstantEventController({ } }}>
- {instantEventState && } + {instantEventState && ( +
+ ( + <> + + {t("seconds")}} + onChange={(e) => { + onChange(Math.abs(Number(e.target.value))); + }} + data-testid="instant-meeting-expiry-time-offset" + /> + + )} + /> + +
+ )}
@@ -213,9 +249,6 @@ const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) => ) : ( <> -

- {t("warning_payment_instant_meeting_event")} -

{ 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, diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 5ac48d34e0..4751ce8779 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -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", diff --git a/packages/features/bookings/Booker/components/hooks/useBookings.ts b/packages/features/bookings/Booker/components/hooks/useBookings.ts index 59fb8b5442..4ae75c003e 100644 --- a/packages/features/bookings/Booker/components/hooks/useBookings.ts +++ b/packages/features/bookings/Booker/components/hooks/useBookings.ts @@ -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" }); }, }); diff --git a/packages/features/eventtypes/lib/types.ts b/packages/features/eventtypes/lib/types.ts index 4f7495cec6..0363b86891 100644 --- a/packages/features/eventtypes/lib/types.ts +++ b/packages/features/eventtypes/lib/types.ts @@ -34,6 +34,7 @@ export type FormValues = { eventName: string; slug: string; isInstantEvent: boolean; + instantMeetingExpiryTimeOffsetInSeconds: number; length: number; offsetStart: number; description: string; diff --git a/packages/features/instant-meeting/handleInstantMeeting.ts b/packages/features/instant-meeting/handleInstantMeeting.ts index 5385e93789..82c4ab4364 100644 --- a/packages/features/instant-meeting/handleInstantMeeting.ts +++ b/packages/features/instant-meeting/handleInstantMeeting.ts @@ -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, diff --git a/packages/lib/event-types/getEventTypeById.ts b/packages/lib/event-types/getEventTypeById.ts index 13678b02f5..baf36fdfc8 100644 --- a/packages/lib/event-types/getEventTypeById.ts +++ b/packages/lib/event-types/getEventTypeById.ts @@ -84,6 +84,7 @@ export const getEventTypeById = async ({ description: true, length: true, isInstantEvent: true, + instantMeetingExpiryTimeOffsetInSeconds: true, aiPhoneCallConfig: true, offsetStart: true, hidden: true, diff --git a/packages/lib/server/eventTypeSelect.ts b/packages/lib/server/eventTypeSelect.ts index 75dc5f6a4a..a0174cab0d 100644 --- a/packages/lib/server/eventTypeSelect.ts +++ b/packages/lib/server/eventTypeSelect.ts @@ -40,6 +40,7 @@ export const eventTypeSelect = Prisma.validator()({ slotInterval: true, successRedirectUrl: true, isInstantEvent: true, + instantMeetingExpiryTimeOffsetInSeconds: true, aiPhoneCallConfig: true, assignAllTeamMembers: true, recurringEvent: true, diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts index d3083672c8..dd79891e4b 100644 --- a/packages/lib/test/builder.ts +++ b/packages/lib/test/builder.ts @@ -82,6 +82,7 @@ export const buildEventType = (eventType?: Partial): EventType => { description: faker.lorem.paragraph(), position: 1, isInstantEvent: false, + instantMeetingExpiryTimeOffsetInSeconds: 90, locations: null, length: 15, offsetStart: 0, diff --git a/packages/platform/sdk/src/endpoints/events/event-types/types.ts b/packages/platform/sdk/src/endpoints/events/event-types/types.ts index 2586a9acf9..9d88567fb7 100644 --- a/packages/platform/sdk/src/endpoints/events/event-types/types.ts +++ b/packages/platform/sdk/src/endpoints/events/event-types/types.ts @@ -59,6 +59,7 @@ export type EventType = { bookingLimits: number | null; durationLimits: number | null; isInstantEvent: boolean; + instantMeetingExpiryTimeOffsetInSeconds: number; assignAllTeamMembers: boolean; useEventTypeDestinationCalendarEmail: boolean; }; diff --git a/packages/platform/sdk/src/endpoints/events/types.ts b/packages/platform/sdk/src/endpoints/events/types.ts index 87edf50321..af7541f4d4 100644 --- a/packages/platform/sdk/src/endpoints/events/types.ts +++ b/packages/platform/sdk/src/endpoints/events/types.ts @@ -45,6 +45,7 @@ export type Event = { eventName: string; slug: string; isInstantEvent: boolean; + instantMeetingExpiryTimeOffsetInSeconds: number; aiPhoneCallConfig: { eventTypeId: number; enabled: boolean; diff --git a/packages/prisma/migrations/20240624195855_add_instant_meeting_expiry_offset/migration.sql b/packages/prisma/migrations/20240624195855_add_instant_meeting_expiry_offset/migration.sql new file mode 100644 index 0000000000..f9f0ed7ca0 --- /dev/null +++ b/packages/prisma/migrations/20240624195855_add_instant_meeting_expiry_offset/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "EventType" ADD COLUMN "instantMeetingExpiryTimeOffsetInSeconds" INTEGER NOT NULL DEFAULT 90; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index b0a3e60d78..9df2e32a9b 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -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) diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index 0ef548f7c0..e97238d929 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -608,6 +608,7 @@ export const allManagedEventTypeProps: { [k in keyof Omit