Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6e520ad
fix: phone-input
hemantmm Sep 10, 2025
6a08127
Merge branch 'main' into phone-input-integration
hemantmm Sep 10, 2025
da3b530
Merge branch 'main' into phone-input-integration
Devanshusharma2005 Sep 10, 2025
c70820f
Merge branch 'main' into phone-input-integration
hemantmm Sep 10, 2025
ae2f6c9
Merge branch 'main' into phone-input-integration
hemantmm Sep 10, 2025
5e9a09e
Merge branch 'main' into phone-input-integration
hemantmm Sep 11, 2025
d863b6d
Merge branch 'main' into phone-input-integration
hemantmm Sep 14, 2025
672a1bd
Merge branch 'main' into phone-input-integration
hemantmm Sep 15, 2025
7625491
Merge branch 'main' into phone-input-integration
hemantmm Sep 15, 2025
6bfe343
fix: comment issue
hemantmm Sep 15, 2025
a8a7d97
Merge branch 'main' into phone-input-integration
hemantmm Sep 15, 2025
bf081e8
Merge branch 'main' into phone-input-integration
hemantmm Sep 17, 2025
37d5f40
Merge branch 'main' into phone-input-integration
hemantmm Sep 17, 2025
087a678
fix: typechecks
hemantmm Sep 17, 2025
d3a3cb8
fix: coderabbit suggestion
hemantmm Sep 17, 2025
b6d1f15
fix: unnecessary code
hemantmm Sep 17, 2025
ec32363
Merge branch 'main' into phone-input-integration
hemantmm Sep 17, 2025
d0334c6
Merge branch 'main' into phone-input-integration
hemantmm Sep 19, 2025
3261a79
Merge branch 'main' into phone-input-integration
Devanshusharma2005 Sep 20, 2025
a0267b0
fix: remove comments
hemantmm Sep 20, 2025
a5470e8
Merge branch 'main' into phone-input-integration
hemantmm Sep 23, 2025
4dbb717
Merge branch 'main' into phone-input-integration
hemantmm Sep 24, 2025
056fc84
Merge upstream/main into phone-input-integration
devin-ai-integration[bot] Jan 15, 2026
44d10b8
fix: consolidate phone input fields and preserve user's required setting
devin-ai-integration[bot] Jan 15, 2026
ae9d4f4
Merge branch 'main' into phone-input-integration
hemantmm Jan 24, 2026
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { useFormContext } from "react-hook-form";

import type { LocationObject } from "@calcom/app-store/locations";
import { getOrganizerInputLocationTypes } from "@calcom/app-store/locations";
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
Expand All @@ -25,21 +23,16 @@ export const BookingFields = ({
isDynamicGroupBooking: boolean;
}) => {
const { t } = useLocale();
const { watch, setValue } = useFormContext();
const locationResponse = watch("responses.location");
const currentView = rescheduleUid ? "reschedule" : "";
const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting);

return (
// TODO: It might make sense to extract this logic into BookingFields config, that would allow to quickly configure system fields and their editability in fresh booking and reschedule booking view
// The logic here intends to make modifications to booking fields based on the way we want to specifically show Booking Form
<div>
{fields.map((field, index) => {
// Don't Display Location field in case of instant meeting as only Cal Video is supported
if (isInstantMeeting && field.name === "location") return null;

// During reschedule by default all system fields are readOnly. Make them editable on case by case basis.
// Allowing a system field to be edited might require sending emails to attendees, so we need to be careful
const rescheduleReadOnly =
(field.editable === "system" || field.editable === "system-but-optional") &&
!!rescheduleUid &&
Expand All @@ -48,10 +41,9 @@ export const BookingFields = ({
const bookingReadOnly = field.editable === "user-readonly";

let readOnly = bookingReadOnly || rescheduleReadOnly;

let hidden = !!field.hidden;
const fieldViews = field.views;

const fieldViews = field.views;
if (fieldViews && !fieldViews.find((view) => view.id === currentView)) {
return null;
}
Expand All @@ -60,29 +52,22 @@ export const BookingFields = ({
if (bookingData === null) {
return null;
}
// rescheduleReason is a reschedule specific field and thus should be editable during reschedule
readOnly = false;
readOnly = false; // rescheduleReason is editable during reschedule
}

if (field.name === SystemField.Enum.smsReminderNumber) {
// `smsReminderNumber` and location.optionValue when location.value===phone are the same data point. We should solve it in a better way in the Form Builder itself.
// I think we should have a way to connect 2 fields together and have them share the same value in Form Builder
if (locationResponse?.value === "phone") {
setValue(`responses.${SystemField.Enum.smsReminderNumber}`, locationResponse?.optionValue);
// Just don't render the field now, as the value is already connected to attendee phone location
return null;
}
// `smsReminderNumber` can be edited during reschedule even though it's a system field
readOnly = false;
// 🚨 Skip duplicate/legacy phone fields (we only want attendeePhoneNumber now)
if (
field.name === SystemField.Enum.smsReminderNumber ||
field.name === SystemField.Enum.aiAgentCallPhoneNumber
) {
return null;
}

if (field.name === SystemField.Enum.guests) {
readOnly = false;
// No matter what user configured for Guests field, we don't show it for dynamic group booking as that doesn't support guests
hidden = isDynamicGroupBooking ? true : !!field.hidden;
}

// We don't show `notes` field during reschedule but since it's a query param we better valid if rescheduleUid brought any bookingData
if (field.name === SystemField.Enum.notes && bookingData !== null) {
return null;
}
Expand All @@ -98,7 +83,6 @@ export const BookingFields = ({
}
const optionsInputs = field.optionsInputs;

// TODO: Instead of `getLocationOptionsForSelect` options should be retrieved from dataStore[field.getOptionsAt]. It would make it agnostic of the `name` of the field.
const options = getLocationOptionsForSelect(locations, t);
options.forEach((option) => {
const optionInput = optionsInputs[option.value as keyof typeof optionsInputs];
Expand Down
76 changes: 59 additions & 17 deletions packages/features/bookings/lib/getBookingFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ export const getBookingFieldsWithSystemFields = ({
export const ensureBookingInputsHaveSystemFields = ({
bookingFields,
disableGuests,
isOrgTeamEvent,
disableBookingTitle,
additionalNotesRequired,
customInputs,
Expand Down Expand Up @@ -146,17 +145,35 @@ export const ensureBookingInputsHaveSystemFields = ({
[EventTypeCustomInputType.PHONE]: BookingFieldTypeEnum.phone,
};

const smsNumberSources = [] as NonNullable<(typeof bookingFields)[number]["sources"]>;
const phoneNumberSources = [] as NonNullable<(typeof bookingFields)[number]["sources"]>;
let isPhoneNumberRequired = false;

workflows.forEach((workflow) => {
workflow.workflow.steps.forEach((step) => {
if (step.action === "SMS_ATTENDEE" || step.action === "WHATSAPP_ATTENDEE") {
const workflowId = workflow.workflow.id;
smsNumberSources.push(
phoneNumberSources.push(
getSmsReminderNumberSource({
workflowId,
isSmsReminderNumberRequired: !!step.numberRequired,
})
);
if (step.numberRequired) {
isPhoneNumberRequired = true;
}
}
// Check for AI agent call workflows that require phone numbers
if (step.action === "CAL_AI_PHONE_CALL") {
const workflowId = workflow.workflow.id;
phoneNumberSources.push(
getAIAgentCallPhoneNumberSource({
workflowId,
isAIAgentCallPhoneNumberRequired: !!step.numberRequired,
})
);
if (step.numberRequired) {
isPhoneNumberRequired = true;
}
Comment on lines +162 to +173
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

CAL_AI_PHONE_CALL must always require a phone number.

Per workflows behavior, AI phone calls require phone regardless of step.numberRequired. Current code makes it conditional.

Apply:

       if (step.action === "CAL_AI_PHONE_CALL") {
         const workflowId = workflow.workflow.id;
         phoneNumberSources.push(
           getAIAgentCallPhoneNumberSource({
             workflowId,
-            isAIAgentCallPhoneNumberRequired: !!step.numberRequired,
+            isAIAgentCallPhoneNumberRequired: true,
           })
         );
-        if (step.numberRequired) {
-          isPhoneNumberRequired = true;
-        }
+        isPhoneNumberRequired = true;
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check for AI agent call workflows that require phone numbers
if (step.action === "CAL_AI_PHONE_CALL") {
const workflowId = workflow.workflow.id;
phoneNumberSources.push(
getAIAgentCallPhoneNumberSource({
workflowId,
isAIAgentCallPhoneNumberRequired: !!step.numberRequired,
})
);
if (step.numberRequired) {
isPhoneNumberRequired = true;
}
// Check for AI agent call workflows that require phone numbers
if (step.action === "CAL_AI_PHONE_CALL") {
const workflowId = workflow.workflow.id;
phoneNumberSources.push(
getAIAgentCallPhoneNumberSource({
workflowId,
isAIAgentCallPhoneNumberRequired: true,
})
);
isPhoneNumberRequired = true;
}
🤖 Prompt for AI Agents
In packages/features/bookings/lib/getBookingFields.ts around lines 165 to 176,
the code treats CAL_AI_PHONE_CALL as requiring a phone only when
step.numberRequired is true; change it so AI phone call steps always require a
phone: when step.action === "CAL_AI_PHONE_CALL" always push
getAIAgentCallPhoneNumberSource with isAIAgentCallPhoneNumberRequired set to
true (or !!true) and set isPhoneNumberRequired = true unconditionally (remove
the conditional check on step.numberRequired).

}
});
});
Expand Down Expand Up @@ -336,21 +353,46 @@ export const ensureBookingInputsHaveSystemFields = ({

bookingFields = missingSystemBeforeFields.concat(bookingFields);

// Backward Compatibility for SMS Reminder Number
// Note: We still need workflows in `getBookingFields` due to Backward Compatibility. If we do a one time entry for all event-types, we can remove workflows from `getBookingFields`
// Also, note that even if Workflows don't explicitly add smsReminderNumber field to bookingFields, it would be added as a side effect of this backward compatibility logic
if (
smsNumberSources.length &&
!bookingFields.find((f) => getFieldIdentifier(f.name) !== getFieldIdentifier(SMS_REMINDER_NUMBER_FIELD))
) {
const indexForLocation = bookingFields.findIndex(
(f) => getFieldIdentifier(f.name) === getFieldIdentifier("location")
// Consolidated Phone Number Logic
// If any workflow requires a phone number, make the attendeePhoneNumber field visible and add sources
if (phoneNumberSources.length) {
const attendeePhoneNumberIndex = bookingFields.findIndex(
(f) => getFieldIdentifier(f.name) === getFieldIdentifier("attendeePhoneNumber")
);
// Add the SMS Reminder Number field after `location` field always
bookingFields.splice(indexForLocation + 1, 0, {
...getSmsReminderNumberField(),
sources: smsNumberSources,
});

if (attendeePhoneNumberIndex !== -1) {
// Update the existing attendeePhoneNumber field to be visible and add sources
bookingFields[attendeePhoneNumberIndex] = {
...bookingFields[attendeePhoneNumberIndex],
hidden: false,
required: isPhoneNumberRequired,
sources: [...(bookingFields[attendeePhoneNumberIndex].sources || []), ...phoneNumberSources],
};
}
}

// Consolidation Logic: If there are existing separate phone fields, make attendeePhoneNumber visible
// This ensures users see only one phone input instead of multiple
const hasExistingSmsField = bookingFields.find(
(f) => getFieldIdentifier(f.name) === getFieldIdentifier(SMS_REMINDER_NUMBER_FIELD)
);
const hasExistingAiField = bookingFields.find(
(f) => getFieldIdentifier(f.name) === getFieldIdentifier(CAL_AI_AGENT_PHONE_NUMBER_FIELD)
);

if (hasExistingSmsField || hasExistingAiField) {
// Make sure attendeePhoneNumber is visible to consolidate the phone inputs
const attendeePhoneNumberIndex = bookingFields.findIndex(
(f) => getFieldIdentifier(f.name) === getFieldIdentifier("attendeePhoneNumber")
);

if (attendeePhoneNumberIndex !== -1) {
bookingFields[attendeePhoneNumberIndex] = {
...bookingFields[attendeePhoneNumberIndex],
hidden: false,
required: bookingFields[attendeePhoneNumberIndex].required || isPhoneNumberRequired,
};
}
}

// Backward Compatibility: If we are migrating from old system, we need to map `customInputs` to `bookingFields`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { v4 as uuidv4 } from "uuid";

import dayjs from "@calcom/dayjs";
import { CAL_AI_AGENT_PHONE_NUMBER_FIELD } from "@calcom/features/bookings/lib/SystemField";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import tasker from "@calcom/features/tasker";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
Expand All @@ -18,12 +17,6 @@ type timeUnitLowerCase = "day" | "hour" | "minute";
function extractPhoneNumber(responses: BookingInfo["responses"]): string | undefined {
if (!responses) return undefined;

// Priority 1: CAL_AI_AGENT_PHONE_NUMBER_FIELD first
const aiAgentPhoneResponse = responses[CAL_AI_AGENT_PHONE_NUMBER_FIELD];
if (aiAgentPhoneResponse && typeof aiAgentPhoneResponse === "object" && "value" in aiAgentPhoneResponse) {
return aiAgentPhoneResponse.value as string;
}

// Priority 2: attendeePhoneNumber as fallback
const attendeePhoneResponse = responses.attendeePhoneNumber;
if (
Expand Down
Loading