Skip to content
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
2 changes: 1 addition & 1 deletion apps/web/pages/api/book/instant-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ async function handler(req: NextApiRequest & { userId?: number }) {
const userIp = getIP(req);

await checkRateLimitAndThrowError({
rateLimitingType: "core",
rateLimitingType: "instantMeeting",
identifier: `instant.event-${piiHasher.hash(userIp)}`,
});

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 @@ -2301,6 +2301,7 @@
"locked_by_team_admin": "Locked by team admin",
"app_not_connected": "You have not connected a {{appName}} account.",
"connect_now": "Connect now",
"connect_now_unavailable_tooltip": "Connect now is unavailable right now. Please book from above slots or try again later.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT:
"Connect now is unavailable right now" sounds off. perhaps "Connect now is currently unavailable"?

"managed_event_dialog_confirm_button_one": "Replace & notify {{count}} member",
"managed_event_dialog_confirm_button_other": "Replace & notify {{count}} members",
"managed_event_dialog_title_one": "The url /{{slug}} already exists for {{count}} member. Do you want to replace it?",
Expand Down
15 changes: 11 additions & 4 deletions packages/features/bookings/Booker/Booker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ const BookerComponent = ({
);

const selectedDate = useBookerStoreContext((state) => state.selectedDate);
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);

const {
shouldShowFormInDialog,
Expand Down Expand Up @@ -145,7 +144,14 @@ const BookerComponent = ({

const { bookerFormErrorRef, key, formEmail, bookingForm, errors: formErrors } = bookerForm;

const { handleBookEvent, errors, loadingStates, expiryTime, instantVideoMeetingUrl } = bookings;
const {
handleBookEvent,
errors,
loadingStates,
expiryTime,
instantVideoMeetingUrl,
instantConnectCooldownMs,
} = bookings;

const watchedCfToken = bookingForm.watch("cfToken");

Expand Down Expand Up @@ -230,9 +236,9 @@ const BookerComponent = ({
setSelectedTimeslot(slot || null);
}, [slot, setSelectedTimeslot]);

const onSubmit = (timeSlot?: string) => {
const onSubmit = (timeSlot?: string) =>
renderConfirmNotVerifyEmailButtonCond ? handleBookEvent(timeSlot) : handleVerifyEmail();
};


const EventBooker = useMemo(() => {
return bookerState === "booking" ? (
Expand Down Expand Up @@ -543,6 +549,7 @@ const BookerComponent = ({
style={{ animationDelay: "1s" }}>
<InstantBooking
event={event.data}
cooldownMs={instantConnectCooldownMs}
onConnectNow={() => {
onConnectNowInstantMeeting();
}}
Expand Down
42 changes: 31 additions & 11 deletions packages/features/bookings/Booker/components/InstantBooking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { User } from "@calcom/prisma/client";
import { UserAvatarGroupWithOrg } from "@calcom/ui/components/avatar";
import { Button } from "@calcom/ui/components/button";
import { Tooltip } from "@calcom/ui/components/tooltip";

interface IInstantBookingProps {
onConnectNow: () => void;
event: Pick<BookerEvent, "entity" | "schedulingType"> & {
subsetOfUsers: (Pick<User, "name" | "username" | "avatarUrl"> & { bookerUrl: string })[];
};
cooldownMs?: number;
}

export const InstantBooking = ({ onConnectNow, event }: IInstantBookingProps) => {
export const InstantBooking = ({ onConnectNow, event, cooldownMs = 0 }: IInstantBookingProps) => {
const { t } = useLocale();
const disabled = cooldownMs > 0;

return (
<div className=" bg-default border-subtle mx-2 block items-center gap-3 rounded-xl border p-[6px] text-sm shadow-sm delay-1000 sm:flex">
Expand All @@ -33,16 +36,33 @@ export const InstantBooking = ({ onConnectNow, event }: IInstantBookingProps) =>
</div>
<div>{t("dont_want_to_wait")}</div>
</div>
<div className="mt-2 sm:mt-0">
<Button
color="primary"
onClick={() => {
onConnectNow();
}}
size="sm"
className="w-full justify-center rounded-lg sm:w-auto">
{t("connect_now")}
</Button>
<div className="mt-2 sm:mt-0 flex items-center gap-3">
{disabled ? (
<Tooltip content={t("connect_now_unavailable_tooltip") || "Connect now is not available right now."}>
<span className="inline-flex">
<Button
disabled={disabled}
color="primary"
onClick={() => {
onConnectNow();
}}
size="sm"
className="w-full justify-center rounded-lg sm:w-auto">
{t("connect_now")}
</Button>
</span>
</Tooltip>
) : (
<Button
color="primary"
onClick={() => {
onConnectNow();
}}
size="sm"
className="w-full justify-center rounded-lg sm:w-auto">
{t("connect_now")}
</Button>
)}
</div>
</div>
);
Expand Down
58 changes: 55 additions & 3 deletions packages/features/bookings/Booker/components/hooks/useBookings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,43 @@ export interface IUseBookingErrors {
export type UseBookingsReturnType = ReturnType<typeof useBookings>;

const STORAGE_KEY = "instantBookingData";
const COOLDOWN_STORAGE_KEY = "instantBookingCooldownByEvent";
const COOLDOWN_WINDOW_MS = 10 * 60 * 1000; // 10 minutes

type InstantBookingCooldownMap = Record<string, number>;

const readInstantCooldownMap = (): InstantBookingCooldownMap => {
try {
const raw = localStorage.getItem(COOLDOWN_STORAGE_KEY);
return raw ? (JSON.parse(raw) as InstantBookingCooldownMap) : {};
} catch {
return {};
}
};

const writeInstantCooldownMap = (map: InstantBookingCooldownMap) => {
try {
localStorage.setItem(COOLDOWN_STORAGE_KEY, JSON.stringify(map));
} catch {
// don't do anything
}
};

const getInstantCooldownRemainingMs = (eventTypeId?: number | null): number => {
if (!eventTypeId) return 0;
const map = readInstantCooldownMap();
const lastTs = map[String(eventTypeId)];
if (!lastTs) return 0;
const remaining = lastTs + COOLDOWN_WINDOW_MS - Date.now();
return remaining > 0 ? remaining : 0;
};

const setInstantCooldownNow = (eventTypeId?: number | null) => {
if (!eventTypeId) return;
const map = readInstantCooldownMap();
map[String(eventTypeId)] = Date.now();
writeInstantCooldownMap(map);
};

const storeInLocalStorage = ({
eventTypeId,
Expand All @@ -133,7 +170,6 @@ export const useBookings = ({
hashedLink,
bookingForm,
metadata,
teamMemberEmail,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we remove it, was it unused?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

isBookingDryRun,
}: IUseBookings) => {
const router = useRouter();
Expand Down Expand Up @@ -179,6 +215,8 @@ export const useBookings = ({
}
}, [eventTypeId, isInstantMeeting]);

const instantConnectCooldownMs = getInstantCooldownRemainingMs(eventTypeId);

const _instantBooking = trpc.viewer.bookings.getInstantBookingLocation.useQuery(
{
bookingId: bookingId,
Expand Down Expand Up @@ -245,7 +283,7 @@ export const useBookings = ({
const { uid, paymentUid } = booking;
const fullName = getFullName(bookingForm.getValues("responses.name"));

const users = !!event.data?.subsetOfHosts?.length
const users = event.data?.subsetOfHosts?.length
? event.data?.subsetOfHosts.map((host) => host.user)
: event.data?.subsetOfUsers;

Expand Down Expand Up @@ -368,6 +406,7 @@ export const useBookings = ({
expiryTime: responseData.expires,
bookingId: responseData.bookingId,
});
setInstantCooldownNow(eventTypeId);
}

updateQueryParam("bookingId", responseData.bookingId);
Expand Down Expand Up @@ -472,7 +511,19 @@ export const useBookings = ({
bookingForm,
hashedLink,
metadata,
handleInstantBooking: createInstantBookingMutation.mutate,
handleInstantBooking: (
variables: Parameters<typeof createInstantBookingMutation.mutate>[0]
) => {
const remaining = getInstantCooldownRemainingMs(eventTypeId);
if (remaining > 0) {
showToast(
t("please_try_again_later_or_book_another_slot"),
"error"
);
return;
}
createInstantBookingMutation.mutate(variables);
},
handleRecBooking: createRecurringBookingMutation.mutate,
handleBooking: createBookingMutation.mutate,
isBookingDryRun,
Expand Down Expand Up @@ -506,5 +557,6 @@ export const useBookings = ({
errors,
loadingStates,
instantVideoMeetingUrl,
instantConnectCooldownMs,
};
};
18 changes: 17 additions & 1 deletion packages/lib/rateLimit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ const log = logger.getSubLogger({ prefix: ["RateLimit"] });
export { type RatelimitResponse };

export type RateLimitHelper = {
rateLimitingType?: "core" | "forcedSlowMode" | "common" | "api" | "ai" | "sms" | "smsMonth";
rateLimitingType?:
| "core"
| "forcedSlowMode"
| "common"
| "api"
| "ai"
| "sms"
| "smsMonth"
| "instantMeeting";
identifier: string;
opts?: LimitOptions;
/**
Expand Down Expand Up @@ -51,6 +59,14 @@ export function rateLimiter() {
timeout,
onError,
}),
instantMeeting: new Ratelimit({
rootKey: UNKEY_ROOT_KEY,
namespace: "instantMeeting",
limit: 1,
duration: "10m",
timeout,
onError,
}),
common: new Ratelimit({
rootKey: UNKEY_ROOT_KEY,
namespace: "common",
Expand Down
Loading