Compare commits

...

4 Commits

Author SHA1 Message Date
GitStart-Cal.com 72728a07b2
chore: Rounding, frames and radio buttons of the booking forms not consistent (#8705)
* Rounding, frames and radio buttons of the booking forms not consistent

* Updates

---------

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-05-25 15:53:42 +00:00
TachyonicBytes 576b2d9876
Add the or operator to the emails package.json as well (#9013)
Co-authored-by: TachyonicBytes <support@tachyonicbytes.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-05-25 15:52:43 +00:00
Udit Takkar 30bdf55399
fix: upgrade btn bug (#9104)
* fix: upgrade btn bug

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: use border-subtle

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-05-25 15:51:13 +00:00
Shivam Kalra 1620a6113a
feat: Add new events to webhook BOOKING_CONFIRMED, BOOKING_REJECTED (#8884)
* test: booking rejection

* test: check if webhook is called

* feat: BOOKING_REJECTED enum ,constant in backend

* feat: send booking rejected webhook to subscribers

* feat: add Booking rejected migration

* calendar event

* Revert "calendar event"

This reverts commit 28d45dccfd.

* feat: BOOKING_REQUESTED enum, constant, migration

* feat: Send BOOKING REQUESTED Webhook call

* feat: Add booking rejected/requested event in form

* feat: data-testid to rejection confirm btn

* test: BOOKING_REJECTED, BOOKING_REQUESTED

* fix: booking status PENDING, Linting

* feat: add new labels to common.json

* remove: meeting ended hook from request hook

* refactor: abstract handleWebhookTrigger

* fix: create a single file for migration

* refactor: reduce code repetition and fix test

* feat: add team webhooks to subscriberOptions

* refactor: subscriberOptions

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-05-25 15:51:11 +00:00
24 changed files with 608 additions and 250 deletions

View File

@ -318,6 +318,7 @@ function BookingListItem(booking: BookingItemProps) {
<DialogClose />
<Button
disabled={mutation.isLoading}
data-testid="rejection-confirm"
onClick={() => {
bookingConfirm(false);
}}>

View File

@ -109,6 +109,7 @@
"react-intl": "^5.25.1",
"react-live-chat-loader": "^2.7.3",
"react-multi-email": "^0.5.3",
"react-phone-input-2": "^2.15.1",
"react-phone-number-input": "^3.2.7",
"react-schemaorg": "^2.0.0",
"react-select": "^5.7.0",

View File

@ -1,131 +1,387 @@
import { expect } from "@playwright/test";
import { test } from "./lib/fixtures";
import { createHttpServer, selectFirstAvailableTimeSlotNextMonth, waitFor } from "./lib/testUtils";
import {
bookOptinEvent,
createHttpServer,
selectFirstAvailableTimeSlotNextMonth,
waitFor,
} from "./lib/testUtils";
test.afterEach(({ users }) => users.deleteAll());
test("add webhook & test that creating an event triggers a webhook call", async ({
page,
users,
}, testInfo) => {
const webhookReceiver = createHttpServer();
const user = await users.create();
const [eventType] = user.eventTypes;
await user.login();
await page.goto(`/settings/developer/webhooks`);
test.describe("BOOKING_CREATED", async () => {
test("add webhook & test that creating an event triggers a webhook call", async ({
page,
users,
}, testInfo) => {
const webhookReceiver = createHttpServer();
const user = await users.create();
const [eventType] = user.eventTypes;
await user.login();
await page.goto(`/settings/developer/webhooks`);
// --- add webhook
await page.click('[data-testid="new_webhook"]');
// --- add webhook
await page.click('[data-testid="new_webhook"]');
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await page.fill('[name="secret"]', "secret");
await Promise.all([
page.click("[type=submit]"),
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
]);
await Promise.all([
page.click("[type=submit]"),
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
]);
// page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
// page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
// --- Book the first available day next month in the pro user's "30min"-event
await page.goto(`/${user.username}/${eventType.slug}`);
await selectFirstAvailableTimeSlotNextMonth(page);
// --- Book the first available day next month in the pro user's "30min"-event
await page.goto(`/${user.username}/${eventType.slug}`);
await selectFirstAvailableTimeSlotNextMonth(page);
// --- fill form
await page.fill('[name="name"]', "Test Testson");
await page.fill('[name="email"]', "test@example.com");
await page.press('[name="email"]', "Enter");
// --- fill form
await page.fill('[name="name"]', "Test Testson");
await page.fill('[name="email"]', "test@example.com");
await page.press('[name="email"]', "Enter");
// --- check that webhook was called
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
// --- check that webhook was called
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
const [request] = webhookReceiver.requestList;
const body = request.body as any;
const [request] = webhookReceiver.requestList;
const body = request.body as any;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
body.payload.location = dynamic;
for (const attendee of body.payload.attendees) {
attendee.timeZone = dynamic;
attendee.language = dynamic;
}
body.payload.organizer.id = dynamic;
body.payload.organizer.email = dynamic;
body.payload.organizer.timeZone = dynamic;
body.payload.organizer.language = dynamic;
body.payload.uid = dynamic;
body.payload.bookingId = dynamic;
body.payload.additionalInformation = dynamic;
body.payload.requiresConfirmation = dynamic;
body.payload.eventTypeId = dynamic;
body.payload.videoCallData = dynamic;
body.payload.appsStatus = dynamic;
body.payload.metadata.videoCallUrl = dynamic;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
body.payload.location = dynamic;
for (const attendee of body.payload.attendees) {
attendee.timeZone = dynamic;
attendee.language = dynamic;
}
body.payload.organizer.id = dynamic;
body.payload.organizer.email = dynamic;
body.payload.organizer.timeZone = dynamic;
body.payload.organizer.language = dynamic;
body.payload.uid = dynamic;
body.payload.bookingId = dynamic;
body.payload.additionalInformation = dynamic;
body.payload.requiresConfirmation = dynamic;
body.payload.eventTypeId = dynamic;
body.payload.videoCallData = dynamic;
body.payload.appsStatus = dynamic;
body.payload.metadata.videoCallUrl = dynamic;
expect(body).toMatchObject({
triggerEvent: "BOOKING_CREATED",
createdAt: "[redacted/dynamic]",
payload: {
type: "30 min",
title: "30 min between Nameless and Test Testson",
description: "",
additionalNotes: "",
customInputs: {},
startTime: "[redacted/dynamic]",
endTime: "[redacted/dynamic]",
organizer: {
id: "[redacted/dynamic]",
name: "Nameless",
email: "[redacted/dynamic]",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
responses: {
email: {
value: "test@example.com",
label: "email_address",
},
name: {
value: "Test Testson",
label: "your_name",
},
},
userFieldsResponses: {},
attendees: [
{
email: "test@example.com",
name: "Test Testson",
expect(body).toMatchObject({
triggerEvent: "BOOKING_CREATED",
createdAt: "[redacted/dynamic]",
payload: {
type: "30 min",
title: "30 min between Nameless and Test Testson",
description: "",
additionalNotes: "",
customInputs: {},
startTime: "[redacted/dynamic]",
endTime: "[redacted/dynamic]",
organizer: {
id: "[redacted/dynamic]",
name: "Nameless",
email: "[redacted/dynamic]",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
],
location: "[redacted/dynamic]",
destinationCalendar: null,
hideCalendarNotes: false,
requiresConfirmation: "[redacted/dynamic]",
eventTypeId: "[redacted/dynamic]",
seatsShowAttendees: true,
seatsPerTimeSlot: null,
uid: "[redacted/dynamic]",
eventTitle: "30 min",
eventDescription: null,
price: 0,
currency: "usd",
length: 30,
bookingId: "[redacted/dynamic]",
metadata: { videoCallUrl: "[redacted/dynamic]" },
status: "ACCEPTED",
additionalInformation: "[redacted/dynamic]",
},
});
responses: {
email: {
value: "test@example.com",
label: "email_address",
},
name: {
value: "Test Testson",
label: "your_name",
},
},
userFieldsResponses: {},
attendees: [
{
email: "test@example.com",
name: "Test Testson",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
],
location: "[redacted/dynamic]",
destinationCalendar: null,
hideCalendarNotes: false,
requiresConfirmation: "[redacted/dynamic]",
eventTypeId: "[redacted/dynamic]",
seatsShowAttendees: true,
seatsPerTimeSlot: null,
uid: "[redacted/dynamic]",
eventTitle: "30 min",
eventDescription: null,
price: 0,
currency: "usd",
length: 30,
bookingId: "[redacted/dynamic]",
metadata: { videoCallUrl: "[redacted/dynamic]" },
status: "ACCEPTED",
additionalInformation: "[redacted/dynamic]",
},
});
webhookReceiver.close();
webhookReceiver.close();
});
});
test.describe("BOOKING_REJECTED", async () => {
test("can book an event that requires confirmation and then that booking can be rejected by organizer", async ({
page,
users,
}) => {
const webhookReceiver = createHttpServer();
// --- create a user
const user = await users.create();
// --- visit user page
await page.goto(`/${user.username}`);
// --- book the user's event
await bookOptinEvent(page);
// --- login as that user
await user.login();
await page.goto(`/settings/developer/webhooks`);
// --- add webhook
await page.click('[data-testid="new_webhook"]');
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await Promise.all([
page.click("[type=submit]"),
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
]);
// page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
await page.goto("/bookings/unconfirmed");
await page.click('[data-testid="reject"]');
await page.click('[data-testid="rejection-confirm"]');
await page.waitForResponse((response) => response.url().includes("/api/trpc/bookings/confirm"));
// --- check that webhook was called
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
const [request] = webhookReceiver.requestList;
const body = request.body as any;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
body.payload.location = dynamic;
for (const attendee of body.payload.attendees) {
attendee.timeZone = dynamic;
attendee.language = dynamic;
}
body.payload.organizer.id = dynamic;
body.payload.organizer.email = dynamic;
body.payload.organizer.timeZone = dynamic;
body.payload.organizer.language = dynamic;
body.payload.uid = dynamic;
body.payload.bookingId = dynamic;
body.payload.additionalInformation = dynamic;
body.payload.requiresConfirmation = dynamic;
body.payload.eventTypeId = dynamic;
body.payload.videoCallData = dynamic;
body.payload.appsStatus = dynamic;
// body.payload.metadata.videoCallUrl = dynamic;
expect(body).toMatchObject({
triggerEvent: "BOOKING_REJECTED",
createdAt: "[redacted/dynamic]",
payload: {
type: "Opt in",
title: "Opt in between Nameless and Test Testson",
customInputs: {},
startTime: "[redacted/dynamic]",
endTime: "[redacted/dynamic]",
organizer: {
id: "[redacted/dynamic]",
name: "Unnamed",
email: "[redacted/dynamic]",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
responses: {
email: {
value: "test@example.com",
label: "email",
},
name: {
value: "Test Testson",
label: "name",
},
},
userFieldsResponses: {},
attendees: [
{
email: "test@example.com",
name: "Test Testson",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
],
location: "[redacted/dynamic]",
destinationCalendar: null,
// hideCalendarNotes: false,
requiresConfirmation: "[redacted/dynamic]",
eventTypeId: "[redacted/dynamic]",
uid: "[redacted/dynamic]",
eventTitle: "Opt in",
eventDescription: null,
price: 0,
currency: "usd",
length: 30,
bookingId: "[redacted/dynamic]",
// metadata: { videoCallUrl: "[redacted/dynamic]" },
status: "REJECTED",
additionalInformation: "[redacted/dynamic]",
},
});
webhookReceiver.close();
});
});
test.describe("BOOKING_REQUESTED", async () => {
test("can book an event that requires confirmation and get a booking requested event", async ({
page,
users,
}) => {
const webhookReceiver = createHttpServer();
// --- create a user
const user = await users.create();
// --- login as that user
await user.login();
await page.goto(`/settings/developer/webhooks`);
// --- add webhook
await page.click('[data-testid="new_webhook"]');
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await Promise.all([
page.click("[type=submit]"),
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
]);
// page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
// --- visit user page
await page.goto(`/${user.username}`);
// --- book the user's opt in
await bookOptinEvent(page);
// --- check that webhook was called
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
const [request] = webhookReceiver.requestList;
const body = request.body as any;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
body.payload.location = dynamic;
for (const attendee of body.payload.attendees) {
attendee.timeZone = dynamic;
attendee.language = dynamic;
}
body.payload.organizer.id = dynamic;
body.payload.organizer.email = dynamic;
body.payload.organizer.timeZone = dynamic;
body.payload.organizer.language = dynamic;
body.payload.uid = dynamic;
body.payload.bookingId = dynamic;
body.payload.additionalInformation = dynamic;
body.payload.requiresConfirmation = dynamic;
body.payload.eventTypeId = dynamic;
body.payload.videoCallData = dynamic;
body.payload.appsStatus = dynamic;
body.payload.metadata.videoCallUrl = dynamic;
expect(body).toMatchObject({
triggerEvent: "BOOKING_REQUESTED",
createdAt: "[redacted/dynamic]",
payload: {
type: "Opt in",
title: "Opt in between Nameless and Test Testson",
customInputs: {},
startTime: "[redacted/dynamic]",
endTime: "[redacted/dynamic]",
organizer: {
id: "[redacted/dynamic]",
name: "Nameless",
email: "[redacted/dynamic]",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
responses: {
email: {
value: "test@example.com",
label: "email_address",
},
name: {
value: "Test Testson",
label: "your_name",
},
},
userFieldsResponses: {},
attendees: [
{
email: "test@example.com",
name: "Test Testson",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
],
location: "[redacted/dynamic]",
destinationCalendar: null,
requiresConfirmation: "[redacted/dynamic]",
eventTypeId: "[redacted/dynamic]",
uid: "[redacted/dynamic]",
eventTitle: "Opt in",
eventDescription: null,
price: 0,
currency: "usd",
length: 30,
bookingId: "[redacted/dynamic]",
status: "PENDING",
additionalInformation: "[redacted/dynamic]",
metadata: { videoCallUrl: "[redacted/dynamic]" },
},
});
webhookReceiver.close();
});
});

View File

@ -374,6 +374,8 @@
"booking_rescheduled": "Booking Rescheduled",
"recording_ready":"Recording Download Link Ready",
"booking_created": "Booking Created",
"booking_rejected":"Booking Rejected",
"booking_requested":"Booking Requested",
"meeting_ended": "Meeting Ended",
"form_submitted": "Form Submitted",
"event_triggers": "Event Triggers",

View File

@ -179,7 +179,7 @@ html.todesktop-platform-darwin.light main.bg-default{
background: rgba(255,255,255,0.8) !important
}
/*
/*
html.todesktop aside a {
height: 28px;
padding: 0px 8px;
@ -497,4 +497,16 @@ select:focus {
-webkit-appearance: none;
}
.react-tel-input .country-list .country:hover,
.react-tel-input .country-list .country.highlight {
@apply !bg-emphasis;
}
.react-tel-input .flag-dropdown .selected-flag,
.react-tel-input .flag-dropdown.open .selected-flag {
@apply !bg-default;
}
.react-tel-input .flag-dropdown {
@apply !border-r-default !border-y-0 !border-l-0 left-0.5;
}

View File

@ -129,7 +129,7 @@ function NumberWidget({ value, setValue, ...remainingProps }: TextLikeComponentP
type="number"
labelSrOnly={remainingProps.noLabel}
containerClassName="w-full"
className="focus:border-brand-default bg-default dark:bg-muted border-default disabled:bg-emphasis focus:ring-brand block w-full rounded-md text-sm disabled:hover:cursor-not-allowed"
className="bg-default border-default disabled:bg-emphasis focus:ring-brand-default dark:focus:border-emphasis block w-full rounded-md text-sm focus:border-neutral-300 disabled:hover:cursor-not-allowed"
value={value}
onChange={(e) => {
setValue(e.target.value);

View File

@ -4,7 +4,7 @@
"version": "0.0.0",
"private": true,
"scripts": {
"dx": "docker compose up -d"
"dx": "docker compose up -d || docker-compose up -d"
},
"dependencies": {
"@calcom/dayjs": "*",

View File

@ -33,6 +33,7 @@ import {
} from "@calcom/emails";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
import {
allowDisablingAttendeeConfirmationEmails,
allowDisablingHostConfirmationEmails,
@ -40,6 +41,7 @@ import {
import { deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
@ -72,7 +74,6 @@ import type { EventResult, PartialReference } from "@calcom/types/EventManager";
import type { WorkingHours, TimeRange as DateOverride } from "@calcom/types/schedule";
import type { EventTypeInfo } from "../../webhooks/lib/sendPayload";
import sendPayload from "../../webhooks/lib/sendPayload";
import getBookingResponsesSchema from "./getBookingResponsesSchema";
const translator = short();
@ -2092,16 +2093,44 @@ async function handler(
videoCallUrl: getVideoCallUrlFromCalEvent(evt),
}
: undefined;
const eventTypeInfo: EventTypeInfo = {
eventTitle: eventType.title,
eventDescription: eventType.description,
requiresConfirmation: requiresConfirmation || null,
price: paymentAppData.price,
currency: eventType.currency,
length: eventType.length,
};
const webhookData = {
...evt,
...eventTypeInfo,
bookingId: booking?.id,
rescheduleUid,
rescheduleStartTime: originalRescheduledBooking?.startTime
? dayjs(originalRescheduledBooking?.startTime).utc().format()
: undefined,
rescheduleEndTime: originalRescheduledBooking?.endTime
? dayjs(originalRescheduledBooking?.endTime).utc().format()
: undefined,
metadata: { ...metadata, ...reqBody.metadata },
eventTypeId,
status: "ACCEPTED",
smsReminderNumber: booking?.smsReminderNumber || undefined,
};
const subscriberOptions: GetSubscriberOptions = {
userId: organizerUser.id,
eventTypeId,
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
teamId: eventType.team?.id,
};
if (isConfirmedByDefault) {
const eventTrigger: WebhookTriggerEvents = rescheduleUid
? WebhookTriggerEvents.BOOKING_RESCHEDULED
: WebhookTriggerEvents.BOOKING_CREATED;
const subscriberOptions = {
userId: organizerUser.id,
eventTypeId,
triggerEvent: eventTrigger,
teamId: eventType.team?.id,
};
subscriberOptions.triggerEvent = eventTrigger;
const subscriberOptionsMeetingEnded = {
userId: organizerUser.id,
@ -2125,48 +2154,14 @@ async function handler(
log.error("Error while running scheduledJobs for booking", error);
}
try {
// Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED
const subscribers = await getWebhooks(subscriberOptions);
console.log("evt:", {
...evt,
metadata: reqBody.metadata,
});
const bookingId = booking?.id;
const eventTypeInfo: EventTypeInfo = {
eventTitle: eventType.title,
eventDescription: eventType.description,
requiresConfirmation: requiresConfirmation || null,
price: paymentAppData.price,
currency: eventType.currency,
length: eventType.length,
};
const promises = subscribers.map((sub) =>
sendPayload(sub.secret, eventTrigger, new Date().toISOString(), sub, {
...evt,
...eventTypeInfo,
bookingId,
rescheduleUid,
rescheduleStartTime: originalRescheduledBooking?.startTime
? dayjs(originalRescheduledBooking?.startTime).utc().format()
: undefined,
rescheduleEndTime: originalRescheduledBooking?.endTime
? dayjs(originalRescheduledBooking?.endTime).utc().format()
: undefined,
metadata: { ...metadata, ...reqBody.metadata },
eventTypeId,
status: "ACCEPTED",
smsReminderNumber: booking?.smsReminderNumber || undefined,
}).catch((e) => {
console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${sub.subscriberUrl}`, e);
})
);
await Promise.all(promises);
} catch (error) {
log.error("Error while sending webhook", error);
}
// Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED
await handleWebhookTrigger({ subscriberOptions, eventTrigger, webhookData });
} else if (eventType.requiresConfirmation) {
// if eventType requires confirmation we will trigger the BOOKING REQUESTED Webhook
const eventTrigger: WebhookTriggerEvents = WebhookTriggerEvents.BOOKING_REQUESTED;
subscriberOptions.triggerEvent = eventTrigger;
webhookData.status = "PENDING";
await handleWebhookTrigger({ subscriberOptions, eventTrigger, webhookData });
}
// Avoid passing referencesToCreate with id unique constrain values

View File

@ -0,0 +1,29 @@
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
import type { WebhookDataType } from "@calcom/features/webhooks/lib/sendPayload";
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
import logger from "@calcom/lib/logger";
export async function handleWebhookTrigger(args: {
subscriberOptions: GetSubscriberOptions;
eventTrigger: string;
webhookData: Omit<WebhookDataType, "createdAt" | "triggerEvent">;
}) {
try {
const subscribers = await getWebhooks(args.subscriberOptions);
const promises = subscribers.map((sub) =>
sendPayload(sub.secret, args.eventTrigger, new Date().toISOString(), sub, args.webhookData).catch(
(e) => {
console.error(
`Error executing webhook for event: ${args.eventTrigger}, URL: ${sub.subscriberUrl}`,
e
);
}
)
);
await Promise.all(promises);
} catch (error) {
logger.error("Error while sending webhook", error);
}
}

View File

@ -1,3 +1,4 @@
import { useRouter } from "next/navigation";
import { useState, Suspense } from "react";
import dayjs from "@calcom/dayjs";
@ -8,14 +9,7 @@ import type { RecordingItemSchema } from "@calcom/prisma/zod-utils";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { PartialReference } from "@calcom/types/EventManager";
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
UpgradeTeamsBadge,
} from "@calcom/ui";
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui";
import { Button } from "@calcom/ui";
import { Download } from "@calcom/ui/components/icon";
@ -101,6 +95,7 @@ const useRecordingDownload = () => {
const ViewRecordingsList = ({ roomName, hasTeamPlan }: { roomName: string; hasTeamPlan: boolean }) => {
const { t } = useLocale();
const { setRecordingId, isFetching, recordingId } = useRecordingDownload();
const router = useRouter();
const { data: recordings } = trpc.viewer.getCalVideoRecordings.useQuery(
{ roomName },
@ -121,7 +116,7 @@ const ViewRecordingsList = ({ roomName, hasTeamPlan }: { roomName: string; hasTe
{recordings.data.map((recording: RecordingItemSchema, index: number) => {
return (
<div
className="flex w-full items-center justify-between rounded-md border px-4 py-2"
className="border-subtle flex w-full items-center justify-between rounded-md border px-4 py-2"
key={recording.id}>
<div className="flex flex-col">
<h1 className="text-sm font-semibold">
@ -138,7 +133,12 @@ const ViewRecordingsList = ({ roomName, hasTeamPlan }: { roomName: string; hasTe
{t("download")}
</Button>
) : (
<UpgradeTeamsBadge />
<Button
tooltip={t("upgrade_to_access_recordings_description")}
className="ml-4 lg:ml-0"
onClick={() => router.push("/teams")}>
{t("upgrade")}
</Button>
)}
</div>
);

View File

@ -1,31 +0,0 @@
import { useRouter } from "next/router";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui";
import { Users } from "@calcom/ui/components/icon";
export default function UpgradeRecordingBanner() {
const { t } = useLocale();
const router = useRouter();
return (
<div className="bg-subtle flex items-start gap-2 rounded-md p-4">
<Users className="dark:bg-gray-90 inline-block h-5 w-5" />
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<h2 className="text-sm font-semibold">{t("upgrade_to_access_recordings_title")}</h2>
<p className="text-sm font-normal">{t("upgrade_to_access_recordings_description")}</p>
</div>
<div>
<Button
onClick={() => {
router.push(`${WEBAPP_URL}/teams`);
}}>
{t("upgrade_now")}
</Button>
</div>
</div>
</div>
);
}

View File

@ -134,7 +134,7 @@ export const Components: Record<BookingFieldType, Component> = {
const { t } = useLocale();
value = value || [];
const inputClassName =
"dark:placeholder:text-darkgray-600 focus:border-brand-default border-subtle block w-full rounded-md border-default text-sm focus:ring-black disabled:bg-emphasis disabled:hover:cursor-not-allowed dark:bg-transparent dark:selection:bg-green-500 disabled:dark:text-subtle";
"dark:placeholder:text-muted focus:border-emphasis border-subtle block w-full rounded-md border-default text-sm focus:ring-black disabled:bg-emphasis disabled:hover:cursor-not-allowed dark:selection:bg-green-500 disabled:dark:text-subtle bg-default";
return (
<>
{value.length ? (
@ -148,11 +148,11 @@ export const Components: Record<BookingFieldType, Component> = {
<EmailField
disabled={readOnly}
value={value[index]}
className={inputClassName}
onChange={(e) => {
value[index] = e.target.value;
setValue(value);
}}
className={inputClassName}
placeholder={placeholder}
label={<></>}
required
@ -251,7 +251,7 @@ export const Components: Record<BookingFieldType, Component> = {
}
setValue(newValue);
}}
className="dark:bg-darkgray-300 border-subtle text-emphasis h-4 w-4 rounded focus:ring-black ltr:mr-2 rtl:ml-2"
className="border-default dark:border-default hover:bg-subtle checked:hover:bg-brand-default checked:bg-brand-default dark:checked:bg-brand-default dark:bg-darkgray-100 dark:hover:bg-subtle dark:checked:hover:bg-brand-default h-4 w-4 cursor-pointer rounded ltr:mr-2 rtl:ml-2"
value={option.value}
checked={value.includes(option.value)}
/>
@ -310,7 +310,7 @@ export const Components: Record<BookingFieldType, Component> = {
type="radio"
disabled={readOnly}
name={name}
className="dark:bg-darkgray-300 border-subtle text-emphasis h-4 w-4 focus:ring-black ltr:mr-2 rtl:ml-2"
className="dark:checked:bg-brand-default dark:bg-darkgray-100 dark:hover:bg-subtle dark:checked:hover:bg-brand-default focus:border-brand-default focus:ring-brand-default border-emphasis h-4 w-4 cursor-pointer text-[--cal-brand] focus:ring-2 ltr:mr-2 rtl:ml-2"
value={option.value}
onChange={(e) => {
setValue({

View File

@ -201,7 +201,7 @@ const SettingsSidebarContainer = ({
alt="User Avatar"
/>
)}
<p className="text-sm font-medium leading-5 truncate">{t(tab.name)}</p>
<p className="truncate text-sm font-medium leading-5">{t(tab.name)}</p>
</div>
</div>
<div className="my-3 space-y-0.5">
@ -226,7 +226,7 @@ const SettingsSidebarContainer = ({
{tab && tab.icon && (
<tab.icon className="h-[16px] w-[16px] stroke-[2px] ltr:mr-3 rtl:ml-3 md:mt-0" />
)}
<p className="text-sm font-medium leading-5 truncate">{t(tab.name)}</p>
<p className="truncate text-sm font-medium leading-5">{t(tab.name)}</p>
</div>
</Link>
{teams &&

View File

@ -32,6 +32,8 @@ const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2: Record<string, WebhookTriggerEve
core: [
{ value: WebhookTriggerEvents.BOOKING_CANCELLED, label: "booking_cancelled" },
{ value: WebhookTriggerEvents.BOOKING_CREATED, label: "booking_created" },
{ value: WebhookTriggerEvents.BOOKING_REJECTED, label: "booking_rejected"},
{ value: WebhookTriggerEvents.BOOKING_REQUESTED, label: "booking_requested"},
{ value: WebhookTriggerEvents.BOOKING_RESCHEDULED, label: "booking_rescheduled" },
{ value: WebhookTriggerEvents.MEETING_ENDED, label: "meeting_ended" },
{ value: WebhookTriggerEvents.RECORDING_READY, label: "recording_ready" },

View File

@ -8,6 +8,8 @@ export const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP = {
WebhookTriggerEvents.BOOKING_CREATED,
WebhookTriggerEvents.BOOKING_RESCHEDULED,
WebhookTriggerEvents.MEETING_ENDED,
WebhookTriggerEvents.BOOKING_REQUESTED,
WebhookTriggerEvents.BOOKING_REJECTED,
WebhookTriggerEvents.RECORDING_READY,
] as const,
"routing-forms": [WebhookTriggerEvents.FORM_SUBMITTED] as const,

View File

@ -16,7 +16,7 @@ export type EventTypeInfo = {
length?: number | null;
};
type WebhookDataType = CalendarEvent &
export type WebhookDataType = CalendarEvent &
EventTypeInfo & {
metadata?: { [key: string]: string };
bookingId?: number;

View File

@ -0,0 +1,3 @@
-- AlterEnum
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'BOOKING_REQUESTED';
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'BOOKING_REJECTED';

View File

@ -495,7 +495,9 @@ enum PaymentOption {
enum WebhookTriggerEvents {
BOOKING_CREATED
BOOKING_RESCHEDULED
BOOKING_REQUESTED
BOOKING_CANCELLED
BOOKING_REJECTED
FORM_SUBMITTED
MEETING_ENDED
RECORDING_READY

View File

@ -4,10 +4,12 @@ import appStore from "@calcom/app-store";
import { sendDeclinedEmails } from "@calcom/emails";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation";
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getTranslation } from "@calcom/lib/server";
import { prisma } from "@calcom/prisma";
import { BookingStatus, MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { BookingStatus, MembershipRole, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { IAbstractPaymentService } from "@calcom/types/PaymentService";
@ -304,6 +306,31 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => {
}
await sendDeclinedEmails(evt);
// send BOOKING_REJECTED webhooks
const subscriberOptions = {
userId: booking.userId,
eventTypeId: booking.eventTypeId,
triggerEvent: WebhookTriggerEvents.BOOKING_REJECTED,
teamId: booking.eventType?.teamId,
};
const eventTrigger: WebhookTriggerEvents = WebhookTriggerEvents.BOOKING_REJECTED;
const eventTypeInfo: EventTypeInfo = {
eventTitle: booking.eventType?.title,
eventDescription: booking.eventType?.description,
requiresConfirmation: booking.eventType?.requiresConfirmation || null,
price: booking.eventType?.price,
currency: booking.eventType?.currency,
length: booking.eventType?.length,
};
const webhookData = {
...evt,
...eventTypeInfo,
bookingId,
eventTypeId: booking.eventType?.id,
status: BookingStatus.REJECTED,
smsReminderNumber: booking.smsReminderNumber || undefined,
};
await handleWebhookTrigger({ subscriberOptions, eventTrigger, webhookData });
}
const message = "Booking " + confirmed ? "confirmed" : "rejected";

View File

@ -23,7 +23,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
{...props}
ref={ref}
className={classNames(
"hover:border-emphasis border-default bg-default placeholder:text-muted text-emphasis disabled:hover:border-default disabled:bg-subtle mb-2 block h-9 rounded-md border py-2 px-3 text-sm leading-4 focus:border-neutral-300 focus:outline-none focus:ring-2 focus:ring-neutral-800 focus:ring-offset-1 disabled:cursor-not-allowed",
"hover:border-emphasis dark:focus:border-emphasis border-default bg-default placeholder:text-muted text-emphasis disabled:hover:border-default disabled:bg-subtle focus:ring-brand-default mb-2 block h-9 rounded-md border py-2 px-3 text-sm leading-4 focus:border-neutral-300 focus:outline-none focus:ring-2 disabled:cursor-not-allowed",
isFullWidth && "w-full",
props.className
)}
@ -129,7 +129,7 @@ export const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function
{addOnLeading || addOnSuffix ? (
<div
dir="ltr"
className="group relative mb-1 flex items-center rounded-md focus-within:outline-none focus-within:ring-2 focus-within:ring-neutral-800 focus-within:ring-offset-1">
className="focus-within:ring-brand-default group relative mb-1 flex items-center rounded-md focus-within:outline-none focus-within:ring-2">
{addOnLeading && (
<Addon isFilled={addOnFilled} className={classNames("rounded-l-md border-r-0", addOnClassname)}>
{addOnLeading}
@ -280,7 +280,7 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(function
ref={ref}
{...props}
className={classNames(
"hover:border-emphasis border-default bg-default placeholder:text-muted text-emphasis disabled:hover:border-default disabled:bg-subtle mb-2 block w-full rounded-md border py-2 px-3 text-sm focus:border-neutral-300 focus:outline-none focus:ring-2 focus:ring-neutral-800 focus:ring-offset-1 disabled:cursor-not-allowed",
"hover:border-emphasis border-default bg-default placeholder:text-muted text-emphasis disabled:hover:border-default disabled:bg-subtle focus:ring-brand-default mb-2 block w-full rounded-md border py-2 px-3 text-sm focus:border-neutral-300 focus:outline-none focus:ring-2 focus:ring-offset-1 disabled:cursor-not-allowed",
props.className
)}
/>

View File

@ -51,7 +51,7 @@ export const Select = <
dropdownIndicator: () => "text-default",
control: (state) =>
cx(
"bg-default border-default !min-h-9 h-9 text-sm leading-4 placeholder:text-sm placeholder:font-normal focus-within:ring-2 focus-within:ring-emphasis hover:border-emphasis rounded-md border ",
"bg-default border-default !min-h-9 h-9 text-sm leading-4 placeholder:text-sm placeholder:font-normal dark:focus:border-emphasis focus-within:outline-none focus-within:ring-2 focus-within:ring-brand-default hover:border-emphasis rounded-md border",
state.isMulti
? variant === "checkbox"
? "px-3 py-2 h-fit"

View File

@ -1,53 +1,69 @@
import { isSupportedCountry } from "libphonenumber-js";
import { useState } from "react";
import BasePhoneInput from "react-phone-number-input";
import type { Props, Country } from "react-phone-number-input";
import "react-phone-number-input/style.css";
import PhoneInput from "react-phone-input-2";
import "react-phone-input-2/lib/style.css";
import { classNames } from "@calcom/lib";
import { trpc } from "@calcom/trpc/react";
export type PhoneInputProps = Props<{
value: string;
export type PhoneInputProps = {
value?: string;
id?: string;
placeholder?: string;
required?: boolean;
className?: string;
name?: string;
}>;
disabled?: boolean;
onChange: (value: string) => void;
};
function PhoneInput({ name, className = "", onChange, ...rest }: PhoneInputProps) {
function BasePhoneInput({ name, className = "", onChange, ...rest }: PhoneInputProps) {
const defaultCountry = useDefaultCountry();
return (
<BasePhoneInput
<PhoneInput
{...rest}
flagUrl="/country-flag-icons/3x2/{XX}.svg"
international
defaultCountry={defaultCountry}
name={name}
onChange={onChange}
countrySelectProps={{ className: "text-emphasis" }}
numberInputProps={{
className: "border-0 text-sm focus:ring-0 bg-default text-default",
country={defaultCountry}
enableSearch
disableSearchIcon
inputProps={{
name: name,
required: rest.required,
placeholder: rest.placeholder,
}}
className={classNames(
"hover:border-emphasis border-default bg-default rounded-md border py-px pl-3 focus-within:border-neutral-300 focus-within:outline-none focus-within:ring-2 focus-within:ring-neutral-800 focus-within:ring-offset-1 disabled:cursor-not-allowed",
onChange={(value) => onChange(value)}
containerClass={classNames(
"hover:border-emphasis dark:focus:border-emphasis border-default !bg-default rounded-md border focus-within:outline-none focus-within:ring-2 focus-within:ring-brand-default disabled:cursor-not-allowed",
className
)}
inputClass="text-sm focus:ring-0 !bg-default text-default"
buttonClass="text-emphasis !bg-default hover:!bg-emphasis"
searchClass="!text-default !bg-default hover:!bg-emphasis"
dropdownClass="!text-default !bg-default"
inputStyle={{ width: "inherit", border: 0 }}
searchStyle={{
display: "flex",
flexDirection: "row",
alignItems: "center",
padding: "6px 12px",
gap: "8px",
width: "296px",
height: "28px",
marginLeft: "-4px",
}}
dropdownStyle={{ width: "max-content" }}
/>
);
}
const useDefaultCountry = () => {
const [defaultCountry, setDefaultCountry] = useState<Country>("US");
const [defaultCountry, setDefaultCountry] = useState("us");
trpc.viewer.public.countryCode.useQuery(undefined, {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: false,
onSuccess: (data) => {
if (isSupportedCountry(data?.countryCode)) {
setDefaultCountry(data.countryCode as Country);
setDefaultCountry(data.countryCode);
}
},
});
@ -55,4 +71,4 @@ const useDefaultCountry = () => {
return defaultCountry;
};
export default PhoneInput;
export default BasePhoneInput;

View File

@ -11,7 +11,7 @@ export const Radio = (props: RadioGroupPrimitive.RadioGroupItemProps & { childre
<RadioGroupPrimitive.Item
{...props}
className={classNames(
"hover:bg-subtle border-default focus:ring-emphasis mt-0.5 h-4 w-4 flex-shrink-0 rounded-full border focus:ring-2",
"hover:bg-subtle border-default dark:checked:bg-brand-default dark:bg-darkgray-100 dark:hover:bg-subtle dark:checked:hover:bg-brand-default focus:ring-brand-default me-1.5 mt-0.5 h-4 w-4 flex-shrink-0 rounded-full border text-[--cal-brand] focus:ring-2 focus:ring-offset-1",
props.disabled && "opacity-60"
)}>
{props.children}
@ -21,7 +21,7 @@ export const Indicator = ({ disabled }: { disabled?: boolean }) => (
<RadioGroupPrimitive.Indicator
className={classNames(
"after:bg-default dark:after:bg-inverted relative flex h-full w-full items-center justify-center rounded-full bg-black after:h-[6px] after:w-[6px] after:rounded-full after:content-['']",
disabled ? "after:bg-muted" : "bg-black"
disabled ? "after:bg-muted" : "bg-brand-default"
)}
/>
);

View File

@ -5212,6 +5212,7 @@ __metadata:
react-intl: ^5.25.1
react-live-chat-loader: ^2.7.3
react-multi-email: ^0.5.3
react-phone-input-2: ^2.15.1
react-phone-number-input: ^3.2.7
react-schemaorg: ^2.0.0
react-select: ^5.7.0
@ -17969,7 +17970,7 @@ __metadata:
languageName: node
linkType: hard
"classnames@npm:^2.2.5":
"classnames@npm:^2.2.5, classnames@npm:^2.2.6":
version: 2.3.2
resolution: "classnames@npm:2.3.2"
checksum: 2c62199789618d95545c872787137262e741f9db13328e216b093eea91c85ef2bfb152c1f9e63027204e2559a006a92eb74147d46c800a9f96297ae1d9f96f4e
@ -28194,6 +28195,13 @@ __metadata:
languageName: node
linkType: hard
"lodash.memoize@npm:^4.1.2":
version: 4.1.2
resolution: "lodash.memoize@npm:4.1.2"
checksum: 9ff3942feeccffa4f1fafa88d32f0d24fdc62fd15ded5a74a5f950ff5f0c6f61916157246744c620173dddf38d37095a92327d5fd3861e2063e736a5c207d089
languageName: node
linkType: hard
"lodash.merge@npm:^4.6.2":
version: 4.6.2
resolution: "lodash.merge@npm:4.6.2"
@ -28215,6 +28223,13 @@ __metadata:
languageName: node
linkType: hard
"lodash.reduce@npm:^4.6.0":
version: 4.6.0
resolution: "lodash.reduce@npm:4.6.0"
checksum: 81f2a1045440554f8427f895ef479f1de5c141edd7852dde85a894879312801efae0295116e5cf830c531c1a51cdab8f3628c3ad39fa21a9874bb9158d9ea075
languageName: node
linkType: hard
"lodash.startcase@npm:^4.4.0":
version: 4.4.0
resolution: "lodash.startcase@npm:4.4.0"
@ -28222,6 +28237,13 @@ __metadata:
languageName: node
linkType: hard
"lodash.startswith@npm:^4.2.1":
version: 4.2.1
resolution: "lodash.startswith@npm:4.2.1"
checksum: 1847371cbf6f32c4125a555847aff8a1ff1a977f40882b0ad4e2656a32e364793cfa7602915c9165cc0bba61fbd802e561888126d242faadcf3fc77215bab19b
languageName: node
linkType: hard
"lodash.uniq@npm:4.5.0":
version: 4.5.0
resolution: "lodash.uniq@npm:4.5.0"
@ -32045,21 +32067,23 @@ __metadata:
languageName: node
linkType: hard
"playwright-core@npm:1.34.0":
version: 1.34.0
resolution: "playwright-core@npm:1.34.0"
checksum: 1821ab254b3c756e52c144c26cae7250dd5a1c779277cdd583bd289501d45cceaea40a8185ebf7eaa56b8291e4a042e3eef65f511da2306349c275794c9a582d
"playwright-core@npm:1.33.0":
version: 1.33.0
resolution: "playwright-core@npm:1.33.0"
bin:
playwright: cli.js
checksum: 5fb7bda06a8b73b56b85b5a0b8f711211dde57a375d9379289e22239b2de879c6d93c8fdc9ba44b932bf100914ab1ca1a55697ad88440fdd0a39101fc020b77f
languageName: node
linkType: hard
"playwright@npm:^1.31.2":
version: 1.34.0
resolution: "playwright@npm:1.34.0"
version: 1.33.0
resolution: "playwright@npm:1.33.0"
dependencies:
playwright-core: 1.34.0
playwright-core: 1.33.0
bin:
playwright: cli.js
checksum: 5e5586e7c4e42f206526fe4306e35c68b7af353d6e3701c9e1045940aa3a916ac50b0b7c27a8eaeb3831ddc4f1146a367f98a5b6b841f977e9770b925e81877f
checksum: fb3934c56ed749cf412ea35b82052f013872d7f7b7747ab7042580af473dfc1b038956a31c132ee8c0098f74150c75e2073e9c737fd4c1ed54700a23fffc079f
languageName: node
linkType: hard
@ -33521,12 +33545,12 @@ __metadata:
linkType: hard
"react-fast-marquee@npm:^1.3.5":
version: 1.6.0
resolution: "react-fast-marquee@npm:1.6.0"
version: 1.5.2
resolution: "react-fast-marquee@npm:1.5.2"
peerDependencies:
react: ">= 16.8.0 || 18.0.0"
react-dom: ">= 16.8.0 || 18.0.0"
checksum: f448341d30ef417292d8f7618d4931190a6d4aa353fbd276da4dcecc49bb6a7a32962f9cc74fe0af2ea9617d7b453ed3efccf7a0e8709a8b66e04ec04f4ae18c
checksum: 2db7a61470a9e02707aa5a80792ec009a8694ef7283b01e30ec516893297794d7f466ae3230c59773bab357684803767c1ffab875175471c41b28a5055345130
languageName: node
linkType: hard
@ -33756,6 +33780,23 @@ __metadata:
languageName: node
linkType: hard
"react-phone-input-2@npm:^2.15.1":
version: 2.15.1
resolution: "react-phone-input-2@npm:2.15.1"
dependencies:
classnames: ^2.2.6
lodash.debounce: ^4.0.8
lodash.memoize: ^4.1.2
lodash.reduce: ^4.6.0
lodash.startswith: ^4.2.1
prop-types: ^15.7.2
peerDependencies:
react: ^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0
react-dom: ^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0
checksum: 60cee455c7044da648a23312cae0077b4108dd24e26173a954904123decefae9875a1f8225a722a6c4d74c37b18ad3038ec6c302b64857f75089e544e8d48847
languageName: node
linkType: hard
"react-phone-number-input@npm:^3.2.7":
version: 3.2.7
resolution: "react-phone-number-input@npm:3.2.7"