diff --git a/apps/web/components/eventtype/InstantEventController.tsx b/apps/web/components/eventtype/InstantEventController.tsx index 9a0b42ee2d..bff93027ab 100644 --- a/apps/web/components/eventtype/InstantEventController.tsx +++ b/apps/web/components/eventtype/InstantEventController.tsx @@ -1,3 +1,4 @@ +import type { Webhook } from "@prisma/client"; import { useSession } from "next-auth/react"; import type { EventTypeSetup, FormValues } from "pages/event-types/[type]"; import { useState } from "react"; @@ -5,10 +6,16 @@ import { useFormContext } from "react-hook-form"; import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager"; +import { WebhookForm } from "@calcom/features/webhooks/components"; +import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm"; +import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem"; +import { subscriberUrlReserved } from "@calcom/features/webhooks/lib/subscriberUrlReserved"; import { classNames } from "@calcom/lib"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Alert, Button, EmptyScreen, SettingsToggle } from "@calcom/ui"; -import { PhoneCall } from "@calcom/ui/components/icon"; +import { WebhookTriggerEvents } from "@calcom/prisma/enums"; +import { trpc } from "@calcom/trpc/react"; +import { Alert, Button, EmptyScreen, SettingsToggle, Dialog, DialogContent, showToast } from "@calcom/ui"; +import { PhoneCall, Plus, Lock, Webhook as TbWebhook } from "@calcom/ui/components/icon"; type InstantEventControllerProps = { eventType: EventTypeSetup; @@ -82,11 +89,7 @@ export default function InstantEventController({ } }}>
- {instantEventState && ( -
-

{t("warning_payment_instant_meeting_event")}

-
- )} + {instantEventState && }
@@ -97,3 +100,201 @@ export default function InstantEventController({ ); } + +const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) => { + const { t } = useLocale(); + const utils = trpc.useContext(); + const { data: webhooks } = trpc.viewer.webhook.list.useQuery({ + eventTypeId: eventType.id, + eventTriggers: [WebhookTriggerEvents.INSTANT_MEETING], + }); + const { data: installedApps, isLoading } = trpc.viewer.integrations.useQuery({ + variant: "other", + onlyInstalled: true, + }); + + const [createModalOpen, setCreateModalOpen] = useState(false); + const [editModalOpen, setEditModalOpen] = useState(false); + const [webhookToEdit, setWebhookToEdit] = useState(); + + const editWebhookMutation = trpc.viewer.webhook.edit.useMutation({ + async onSuccess() { + setEditModalOpen(false); + await utils.viewer.webhook.list.invalidate(); + showToast(t("webhook_updated_successfully"), "success"); + }, + onError(error) { + showToast(`${error.message}`, "error"); + }, + }); + + const createWebhookMutation = trpc.viewer.webhook.create.useMutation({ + async onSuccess() { + showToast(t("webhook_created_successfully"), "success"); + await utils.viewer.webhook.list.invalidate(); + setCreateModalOpen(false); + }, + onError(error) { + showToast(`${error.message}`, "error"); + }, + }); + + const onCreateWebhook = async (values: WebhookFormSubmitData) => { + if ( + subscriberUrlReserved({ + subscriberUrl: values.subscriberUrl, + id: values.id, + webhooks, + eventTypeId: eventType.id, + }) + ) { + showToast(t("webhook_subscriber_url_reserved"), "error"); + return; + } + + if (!values.payloadTemplate) { + values.payloadTemplate = null; + } + + createWebhookMutation.mutate({ + subscriberUrl: values.subscriberUrl, + eventTriggers: values.eventTriggers, + active: values.active, + payloadTemplate: values.payloadTemplate, + secret: values.secret, + eventTypeId: eventType.id, + }); + }; + + const NewWebhookButton = () => { + const { t } = useLocale(); + return ( + + ); + }; + + const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager( + eventType, + t("locked_fields_admin_description"), + t("locked_fields_member_description") + ); + const webhookLockedStatus = shouldLockDisableProps("webhooks"); + + return ( +
+ {webhooks && !isLoading && ( + <> +
+ {webhooks.length ? ( + <> +
+ {webhooks.map((webhook, index) => { + return ( + { + setEditModalOpen(true); + setWebhookToEdit(webhook); + }} + /> + ); + })} +
+

+ {t("warning_payment_instant_meeting_event")} +

+ + ) : ( + <> +

+ {t("warning_payment_instant_meeting_event")} +

+ + {t("locked_by_admin")} + + ) : ( + + ) + } + /> + + )} +
+ + {/* New webhook dialog */} + !isOpen && setCreateModalOpen(false)}> + + setCreateModalOpen(false)} + apps={installedApps?.items.map((app) => app.slug)} + selectOnlyInstantMeetingOption={true} + /> + + + {/* Edit webhook dialog */} + !isOpen && setEditModalOpen(false)}> + + app.slug)} + onCancel={() => setEditModalOpen(false)} + onSubmit={(values: WebhookFormSubmitData) => { + if ( + subscriberUrlReserved({ + subscriberUrl: values.subscriberUrl, + id: webhookToEdit?.id, + webhooks, + eventTypeId: eventType.id, + }) + ) { + showToast(t("webhook_subscriber_url_reserved"), "error"); + return; + } + + if (values.changeSecret) { + values.secret = values.newSecret.length ? values.newSecret : null; + } + + if (!values.payloadTemplate) { + values.payloadTemplate = null; + } + + editWebhookMutation.mutate({ + id: webhookToEdit?.id || "", + subscriberUrl: values.subscriberUrl, + eventTriggers: values.eventTriggers, + active: values.active, + payloadTemplate: values.payloadTemplate, + secret: values.secret, + eventTypeId: webhookToEdit?.eventTypeId || undefined, + }); + }} + /> + + + + )} +
+ ); +}; diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 995f977502..0406219a1c 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -112,7 +112,7 @@ "requested_to_reschedule_subject_attendee": "Action Required Reschedule: Please book a new time for {{eventType}} with {{name}}", "hi_user_name": "Hi {{name}}", "ics_event_title": "{{eventType}} with {{name}}", - "please_book_a_time_sometime_later":"No one was available at the moment. Please book for sometime later", + "please_book_a_time_sometime_later":"Oops, we couldn't connect you this time. Please schedule a future call instead", "new_event_subject": "New event: {{attendeeName}} - {{date}} - {{eventType}}", "join_by_entrypoint": "Join by {{entryPoint}}", "notes": "Notes", @@ -440,7 +440,7 @@ "browse_api_documentation": "Browse our API documentation", "leverage_our_api": "Leverage our API for full control and customizability.", "create_webhook": "Create Webhook", - "instant_meeting_created":"Instant Meeting Created", + "instant_meeting":"Instant Meeting Created", "booking_cancelled": "Booking Canceled", "booking_rescheduled": "Booking Rescheduled", "recording_ready": "Recording Download Link Ready", @@ -555,6 +555,7 @@ "enter_number_between_range": "Please enter a number between 1 and {{maxOccurences}}", "email_address": "Email address", "enter_valid_email": "Please enter a valid email", + "please_schedule_future_call":" Please schedule a future call if we're not available in {{seconds}} seconds", "location": "Location", "address": "Address", "enter_address": "Enter address", @@ -1979,6 +1980,7 @@ "connect_google_workspace": "Connect Google Workspace", "google_workspace_admin_tooltip": "You must be a Workspace Admin to use this feature", "first_event_type_webhook_description": "Create your first webhook for this event type", + "create_instant_meeting_webhook_description":"Create your first webhook with 'Instant Meeting Created' as trigger for this event type", "install_app_on": "Install app on", "create_for": "Create for", "currency": "Currency", diff --git a/apps/web/public/static/locales/he/common.json b/apps/web/public/static/locales/he/common.json index 3612ccc431..fd2001b011 100644 --- a/apps/web/public/static/locales/he/common.json +++ b/apps/web/public/static/locales/he/common.json @@ -440,7 +440,7 @@ "browse_api_documentation": "עיון במסמכי ממשק תכנות היישומים (API) שלנו", "leverage_our_api": "מומלץ להיעזר בממשק תכנות היישומים (API) שלנו לקבלת שליטה מלאה ויכולת התאמה אישית.", "create_webhook": "יצירת Webhook", - "instant_meeting_created": "נוצרה פגישה מיידית", + "instant_meeting": "נוצרה פגישה מיידית", "booking_cancelled": "ההזמנה בוטלה", "booking_rescheduled": "מועד ההזמנה השתנה", "recording_ready": "הקישור להורדת ההקלטה מוכן", diff --git a/packages/dayjs/index.ts b/packages/dayjs/index.ts index 12fc0e949a..c2c9a000f5 100644 --- a/packages/dayjs/index.ts +++ b/packages/dayjs/index.ts @@ -2,6 +2,7 @@ import dayjs from "dayjs"; import dayjsBusinessTime from "dayjs-business-days2"; import customParseFormat from "dayjs/plugin/customParseFormat"; +import duration from "dayjs/plugin/duration"; import isBetween from "dayjs/plugin/isBetween"; import isToday from "dayjs/plugin/isToday"; import localizedFormat from "dayjs/plugin/localizedFormat"; @@ -21,6 +22,7 @@ dayjs.extend(timeZone); dayjs.extend(toArray); dayjs.extend(utc); dayjs.extend(minmax); +dayjs.extend(duration); export type Dayjs = dayjs.Dayjs; diff --git a/packages/features/bookings/Booker/Booker.tsx b/packages/features/bookings/Booker/Booker.tsx index 244f799d48..32a8199158 100644 --- a/packages/features/bookings/Booker/Booker.tsx +++ b/packages/features/bookings/Booker/Booker.tsx @@ -179,7 +179,7 @@ const BookerComponent = ({ event, }); - const { handleBookEvent, hasInstantMeetingTokenExpired, errors, loadingStates } = useBookings({ + const { handleBookEvent, hasInstantMeetingTokenExpired, errors, loadingStates, expiryTime } = useBookings({ event, hashedLink, bookingForm, @@ -245,6 +245,7 @@ const BookerComponent = ({ isUserSessionRequiredToVerify={false} /> { @@ -279,6 +280,7 @@ const BookerComponent = ({ setSeatedEventData, setSelectedTimeslot, setVerifiedEmail, + expiryTime, ] ); @@ -301,8 +303,9 @@ const BookerComponent = ({ {bookerState !== "booking" && event.data?.isInstantEvent && (
+ style={{ animationDelay: "1s" }}> { const newPath = `${pathname}?isInstantMeeting=true`; router.push(newPath); diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx index 56047cdff2..2489fbaa85 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx @@ -51,6 +51,7 @@ export const BookEventForm = ({ const timeslot = useBookerStore((state) => state.selectedTimeslot); const username = useBookerStore((state) => state.username); const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting); + const [expiryTime, setExpiryTime] = useState(); const [responseVercelIdHeader] = useState(null); const { t } = useLocale(); diff --git a/packages/features/bookings/Booker/components/InstantBooking.tsx b/packages/features/bookings/Booker/components/InstantBooking.tsx index ff93fb4c4d..a811e25df8 100644 --- a/packages/features/bookings/Booker/components/InstantBooking.tsx +++ b/packages/features/bookings/Booker/components/InstantBooking.tsx @@ -1,21 +1,30 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { SchedulingType } from "@calcom/prisma/enums"; +import { type RouterOutputs } from "@calcom/trpc"; import { Button } from "@calcom/ui"; +import { UserAvatarGroupWithOrg } from "@calcom/web/components/ui/avatar/UserAvatarGroupWithOrg"; interface IInstantBookingProps { onConnectNow: () => void; -} -{ - /* TODO: max. show 4 people here */ - /* use AvatarGroup */ + event: NonNullable; } -export const InstantBooking = ({ onConnectNow }: IInstantBookingProps) => { +export const InstantBooking = ({ onConnectNow, event }: IInstantBookingProps) => { const { t } = useLocale(); return (
+
{t("dont_want_to_wait")}
diff --git a/packages/features/bookings/Booker/components/RedirectToInstantMeetingModal.tsx b/packages/features/bookings/Booker/components/RedirectToInstantMeetingModal.tsx index 51db3c650d..1f96f89eee 100644 --- a/packages/features/bookings/Booker/components/RedirectToInstantMeetingModal.tsx +++ b/packages/features/bookings/Booker/components/RedirectToInstantMeetingModal.tsx @@ -1,3 +1,6 @@ +import { useState, useEffect } from "react"; + +import dayjs from "@calcom/dayjs"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Dialog, DialogContent } from "@calcom/ui"; import { Button } from "@calcom/ui"; @@ -6,15 +9,60 @@ export const RedirectToInstantMeetingModal = ({ hasInstantMeetingTokenExpired, bookingId, onGoBack, + expiryTime, }: { hasInstantMeetingTokenExpired: boolean; bookingId: number; onGoBack: () => void; + expiryTime?: Date; }) => { const { t } = useLocale(); + const [timeRemaining, setTimeRemaining] = useState(calculateTimeRemaining()); + + function calculateTimeRemaining() { + const now = dayjs(); + const expiration = dayjs(expiryTime); + const duration = expiration.diff(now); + return Math.max(duration, 0); + } + + useEffect(() => { + if (!expiryTime) return; + + const timer = setInterval(() => { + setTimeRemaining(calculateTimeRemaining()); + }, 1000); + + return () => { + clearInterval(timer); + }; + }, [expiryTime]); + + const formatTime = (milliseconds: number) => { + const duration = dayjs.duration(milliseconds); + const seconds = duration.seconds(); + const minutes = duration.minutes(); + + return `${minutes}m ${seconds}s`; + }; + + useEffect(() => { + if (hasInstantMeetingTokenExpired) return; + + const handleBeforeUnload = (event: BeforeUnloadEvent) => { + const message = "/o"; + event.returnValue = message; // Standard for most browsers + return message; // For some older browsers + }; + + window.addEventListener("beforeunload", handleBeforeUnload); + return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + }; + }, [hasInstantMeetingTokenExpired]); return ( - +
{hasInstantMeetingTokenExpired ? ( @@ -32,18 +80,14 @@ export const RedirectToInstantMeetingModal = ({ ) : (

{t("connecting_you_to_someone")}

- {/* TODO: Add countdown from 60 seconds - We are connecting you! - Please schedule a future call if we're not available in XX seconds. - */} - - {/* Once countdown ends: - Oops, we couldn't connect you this time. - Please schedule a future call instead. We value your time. - */} -

{t("please_do_not_close_this_tab")}

-
+

+ {t("please_schedule_future_call", { + seconds: formatTime(timeRemaining), + })} +

+ +