diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer.ts index c22902960132ce..971acae8f03c94 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer.ts @@ -17,7 +17,7 @@ export class EventTypeResponseTransformPipe implements PipeTransform { private transformEventType(eventType: EventTypeResponse): EventTypeOutput_2024_06_14 { return plainToClass( EventTypeOutput_2024_06_14, - this.outputEventTypesService.getResponseEventType(eventType.ownerId, eventType, false), + this.outputEventTypesService.getResponseEventType(eventType.ownerId, eventType), { strategy: "exposeAll" } ); } diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts index b0db6cc1ecc7f8..974c682b308fa0 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts @@ -96,11 +96,7 @@ type Input = Pick< @Injectable() export class OutputEventTypesService_2024_06_14 { - getResponseEventType( - ownerId: number, - databaseEventType: Input, - isOrgTeamEvent: boolean - ): EventTypeOutput_2024_06_14 { + getResponseEventType(ownerId: number, databaseEventType: Input): EventTypeOutput_2024_06_14 { const { id, length, @@ -138,7 +134,7 @@ export class OutputEventTypesService_2024_06_14 { const customName = databaseEventType?.eventName ?? undefined; const bookingFields = databaseEventType.bookingFields ? this.transformBookingFields(databaseEventType.bookingFields) - : this.getDefaultBookingFields(isOrgTeamEvent); + : this.getDefaultBookingFields(); const recurrence = this.transformRecurringEvent(databaseEventType.recurringEvent); const metadata = this.transformMetadata(databaseEventType.metadata) || {}; @@ -261,14 +257,13 @@ export class OutputEventTypesService_2024_06_14 { return [...transformBookingFieldsInternalToApi(knownBookingFields), ...unknownBookingFields]; } - getDefaultBookingFields(isOrgTeamEvent: boolean) { + getDefaultBookingFields() { const defaultBookingFields = getBookingFieldsWithSystemFields({ disableGuests: false, bookingFields: null, customInputs: [], metadata: null, workflows: [], - isOrgTeamEvent, }); return this.transformBookingFields(defaultBookingFields); } diff --git a/apps/api/v2/src/modules/organizations/event-types/services/output.service.ts b/apps/api/v2/src/modules/organizations/event-types/services/output.service.ts index 67cf769e1eb2ef..496fb8bfc71d0e 100644 --- a/apps/api/v2/src/modules/organizations/event-types/services/output.service.ts +++ b/apps/api/v2/src/modules/organizations/event-types/services/output.service.ts @@ -93,14 +93,13 @@ export class OutputOrganizationsEventTypesService { private readonly teamsEventTypesRepository: TeamsEventTypesRepository, private readonly usersRepository: UsersRepository ) {} - + // eslint-disable-next-line @typescript-eslint/no-unused-vars async getResponseTeamEventType(databaseEventType: Input, isOrgTeamEvent: boolean) { const { teamId, userId, parentId, assignAllTeamMembers } = databaseEventType; // eslint-disable-next-line @typescript-eslint/no-unused-vars const { ownerId, users, ...rest } = this.outputEventTypesService.getResponseEventType( 0, - databaseEventType, - isOrgTeamEvent + databaseEventType ); const hosts = databaseEventType.schedulingType === "MANAGED" diff --git a/apps/web/lib/booking.ts b/apps/web/lib/booking.ts index 3c61015f0fff52..ad927860af6258 100644 --- a/apps/web/lib/booking.ts +++ b/apps/web/lib/booking.ts @@ -117,12 +117,11 @@ export const getEventTypesFromDB = async (id: number) => { const metadata = EventTypeMetaDataSchema.parse(eventType.metadata); const { profile, ...restEventType } = eventType; - const isOrgTeamEvent = !!eventType?.team && !!profile?.organizationId; return { isDynamic: false, ...restEventType, - bookingFields: getBookingFieldsWithSystemFields({ ...eventType, isOrgTeamEvent }), + bookingFields: getBookingFieldsWithSystemFields({ ...eventType }), metadata, }; }; diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 218524b1fff983..e0f4b966bfc2ef 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -3670,6 +3670,9 @@ "webhook_metadata": "Metadata", "stats": "Stats", "booking_status": "Booking status", + "allowed_country_codes": "Allowed Country Codes", + "select_country_codes": "Select country codes...", + "country_code_restriction_help": "SMS will only be sent to phone numbers from the selected countries. Leave empty to allow all countries.", "visit": "Visit", "location_custom_label_input_label": "Custom label on booking page", "meeting_link": "Meeting link", diff --git a/packages/features/bookings/lib/getBookingFields.ts b/packages/features/bookings/lib/getBookingFields.ts index 150d5cd4d2103e..6f0bd4055a1329 100644 --- a/packages/features/bookings/lib/getBookingFields.ts +++ b/packages/features/bookings/lib/getBookingFields.ts @@ -47,6 +47,7 @@ export const getSmsReminderNumberSource = ({ }: { workflowId: Workflow["id"]; isSmsReminderNumberRequired: boolean; + allowedCountryCodes?: string[]; }) => ({ id: `${workflowId}`, type: "workflow", @@ -84,7 +85,6 @@ export const getAIAgentCallPhoneNumberSource = ({ export const getBookingFieldsWithSystemFields = ({ bookingFields, disableGuests, - isOrgTeamEvent = false, disableBookingTitle, customInputs, metadata, @@ -92,7 +92,6 @@ export const getBookingFieldsWithSystemFields = ({ }: { bookingFields: Fields | EventType["bookingFields"]; disableGuests: boolean; - isOrgTeamEvent?: boolean; disableBookingTitle?: boolean; customInputs: EventTypeCustomInput[] | z.infer[]; metadata: EventType["metadata"] | z.infer; @@ -107,7 +106,6 @@ export const getBookingFieldsWithSystemFields = ({ return ensureBookingInputsHaveSystemFields({ bookingFields: parsedBookingFields, disableGuests, - isOrgTeamEvent, disableBookingTitle, additionalNotesRequired: parsedMetaData?.additionalNotesRequired || false, customInputs: parsedCustomInputs, @@ -118,7 +116,6 @@ export const getBookingFieldsWithSystemFields = ({ export const ensureBookingInputsHaveSystemFields = ({ bookingFields, disableGuests, - isOrgTeamEvent, disableBookingTitle, additionalNotesRequired, customInputs, @@ -126,7 +123,6 @@ export const ensureBookingInputsHaveSystemFields = ({ }: { bookingFields: Fields; disableGuests: boolean; - isOrgTeamEvent: boolean; disableBookingTitle?: boolean; additionalNotesRequired: boolean; customInputs: z.infer[]; @@ -147,10 +143,12 @@ export const ensureBookingInputsHaveSystemFields = ({ }; const smsNumberSources = [] as NonNullable<(typeof bookingFields)[number]["sources"]>; + workflows.forEach((workflow) => { workflow.workflow.steps.forEach((step) => { if (step.action === "SMS_ATTENDEE" || step.action === "WHATSAPP_ATTENDEE") { const workflowId = workflow.workflow.id; + smsNumberSources.push( getSmsReminderNumberSource({ workflowId, diff --git a/packages/features/bookings/lib/getBookingResponsesSchema.ts b/packages/features/bookings/lib/getBookingResponsesSchema.ts index d3e61a883dde0e..4c51ebc007aeb3 100644 --- a/packages/features/bookings/lib/getBookingResponsesSchema.ts +++ b/packages/features/bookings/lib/getBookingResponsesSchema.ts @@ -1,4 +1,4 @@ -import { isValidPhoneNumber } from "libphonenumber-js"; +import { isValidPhoneNumber, getCountryCallingCode, type CountryCode } from "libphonenumber-js"; import z from "zod"; import type { ALL_VIEWS } from "@calcom/features/form-builder/schema"; @@ -6,7 +6,6 @@ import { dbReadResponseSchema, fieldTypesSchemaMap } from "@calcom/features/form import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils"; import { bookingResponses, emailSchemaRefinement } from "@calcom/prisma/zod-utils"; -// eslint-disable-next-line @typescript-eslint/ban-types type View = ALL_VIEWS | (string & {}); type BookingFields = (z.infer & z.BRAND<"HAS_SYSTEM_FIELDS">) | null; type CommonParams = { bookingFields: BookingFields; view: View }; @@ -103,15 +102,41 @@ function preprocess({ }; try { parsedValue = JSON.parse(value); - } catch (e) {} + } catch (e) { + console.error("Failed to parse JSON:", e); + } const optionsInputs = field.optionsInputs; const optionInputField = optionsInputs?.[parsedValue.value]; if (optionInputField && optionInputField.type === "phone") { - parsedValue.optionValue = ensureValidPhoneNumber(parsedValue.optionValue); + let phoneValue = parsedValue.optionValue; + // Auto-prepend country code for single country restrictions + if (field.allowedCountryCodes?.length === 1 && phoneValue) { + const countryCode = field.allowedCountryCodes[0].toUpperCase(); + const dialCode = getCountryCallingCode(countryCode as CountryCode); + + // If phoneValue doesn't start with + or doesn't start with the correct country code + if (!phoneValue.startsWith(`+${dialCode}`)) { + // Remove any existing + and prepend the correct country code + phoneValue = `+${dialCode}${phoneValue.replace(/^\+/, "")}`; + } + } + parsedValue.optionValue = ensureValidPhoneNumber(phoneValue); } newResponses[field.name] = parsedValue; } else if (field.type === "phone") { - newResponses[field.name] = ensureValidPhoneNumber(value); + // Auto-prepend country code for single country restrictions + let phoneValue = value; + if (field.allowedCountryCodes?.length === 1 && phoneValue) { + const countryCode = field.allowedCountryCodes[0].toUpperCase(); + const dialCode = getCountryCallingCode(countryCode as CountryCode); + + // If phoneValue doesn't start with + or doesn't start with the correct country code + if (!phoneValue.startsWith(`+${dialCode}`)) { + // Remove any existing + and prepend the correct country code + phoneValue = `+${dialCode}${phoneValue.replace(/^\+/, "")}`; + } + } + newResponses[field.name] = ensureValidPhoneNumber(phoneValue); } else { newResponses[field.name] = value; } diff --git a/packages/features/bookings/lib/handleNewBooking/getEventType.ts b/packages/features/bookings/lib/handleNewBooking/getEventType.ts index bb5313375b3994..773e6ff5f8dd2f 100644 --- a/packages/features/bookings/lib/handleNewBooking/getEventType.ts +++ b/packages/features/bookings/lib/handleNewBooking/getEventType.ts @@ -15,11 +15,9 @@ const _getEventType = async ({ const eventType = !eventTypeId && !!eventTypeSlug ? getDefaultEvent(eventTypeSlug) : await getEventTypesFromDB(eventTypeId); - const isOrgTeamEvent = !!eventType?.team && !!eventType?.team?.parentId; - return { ...eventType, - bookingFields: getBookingFieldsWithSystemFields({ ...eventType, isOrgTeamEvent }), + bookingFields: getBookingFieldsWithSystemFields({ ...eventType }), }; }; diff --git a/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts b/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts index 7e60482df243a9..1e184e34307d6c 100644 --- a/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts +++ b/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts @@ -194,10 +194,9 @@ export const getEventTypesFromDB = async (eventTypeId: number) => { throw new Error(ErrorCode.EventTypeNotFound); } + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { profile, hosts, users, ...restEventType } = eventType; - const isOrgTeamEvent = !!eventType?.team && !!profile?.organizationId; - const hostsWithSelectedCalendars = hosts.map((host) => ({ ...host, user: withSelectedCalendars(host.user), @@ -213,7 +212,7 @@ export const getEventTypesFromDB = async (eventTypeId: number) => { recurringEvent: parseRecurringEvent(eventType?.recurringEvent), customInputs: customInputSchema.array().parse(eventType?.customInputs || []), locations: (eventType?.locations ?? []) as LocationObject[], - bookingFields: getBookingFieldsWithSystemFields({ ...restEventType, isOrgTeamEvent }), + bookingFields: getBookingFieldsWithSystemFields({ ...restEventType }), rrSegmentQueryValue: rrSegmentQueryValueSchema.parse(eventType.rrSegmentQueryValue) ?? null, isDynamic: false, hostGroups: eventType.hostGroups || [], diff --git a/packages/features/components/phone-input/PhoneInput.tsx b/packages/features/components/phone-input/PhoneInput.tsx index c7264ea2e062a8..246edd54ae1d1d 100644 --- a/packages/features/components/phone-input/PhoneInput.tsx +++ b/packages/features/components/phone-input/PhoneInput.tsx @@ -20,6 +20,7 @@ export type PhoneInputProps = { disabled?: boolean; onChange: (value: string) => void; defaultCountry?: string; + allowedCountryCodes?: string[]; inputStyle?: CSSProperties; flagButtonStyle?: CSSProperties; }; @@ -30,6 +31,7 @@ function BasePhoneInput({ onChange, value, defaultCountry = "us", + allowedCountryCodes, ...rest }: PhoneInputProps) { const isPlatform = useIsPlatform(); @@ -53,17 +55,34 @@ function BasePhoneInput({ if (!isPlatform) { return ( - + ); } + const singleCountry = allowedCountryCodes?.length === 1; + const onlyCountries = allowedCountryCodes?.map((code) => code.toLowerCase()); + + // If country codes are restricted, use the first one as the default + const effectiveDefaultCountry = allowedCountryCodes?.length + ? allowedCountryCodes[0].toLowerCase() + : defaultCountry; + return ( 0 ? onlyCountries : undefined} + disableCountryCode={singleCountry} inputProps={{ name, required: rest.required, @@ -101,19 +120,29 @@ function BasePhoneInputWeb({ className = "", onChange, value, + allowedCountryCodes, inputStyle, flagButtonStyle, ...rest }: Omit) { const defaultCountry = useDefaultCountry(); + const onlyCountries = allowedCountryCodes?.map((code) => code.toLowerCase()); + const singleCountry = allowedCountryCodes?.length === 1; + + // If country codes are restricted, use the first one as the default + const effectiveDefaultCountry = allowedCountryCodes?.length + ? allowedCountryCodes[0].toLowerCase() + : defaultCountry; return ( 0 ? onlyCountries : undefined} + disableCountryCode={singleCountry} inputProps={{ name, required: rest.required, @@ -162,9 +191,11 @@ const useDefaultCountry = () => { return; } - isSupportedCountry(data?.countryCode) - ? setDefaultCountry(data.countryCode.toLowerCase()) - : setDefaultCountry(navigator.language.split("-")[1]?.toLowerCase() || "us"); + if (isSupportedCountry(data?.countryCode)) { + setDefaultCountry(data.countryCode.toLowerCase()); + } else { + setDefaultCountry(navigator.language.split("-")[1]?.toLowerCase() || "us"); + } }, [query.data] ); diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx index 3802e86e447142..6d767915440c3b 100644 --- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx +++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx @@ -1,7 +1,7 @@ import { type TFunction } from "i18next"; import { useParams, useRouter, useSearchParams } from "next/navigation"; import type { Dispatch, SetStateAction } from "react"; -import { useEffect, useRef, useState, useCallback } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { UseFormReturn } from "react-hook-form"; import { Controller } from "react-hook-form"; import "react-phone-number-input/style.css"; @@ -102,7 +102,7 @@ const getTimeSectionText = (trigger: WorkflowTriggerEvents, t: TFunction) => { [WorkflowTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW]: "how_long_after_guests_no_show", }; if (!triggerMap[trigger]) return null; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return t(triggerMap[trigger]!); }; @@ -236,8 +236,11 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { }, }); - const verifiedNumbers = _verifiedNumbers?.map((number) => number.phoneNumber) || []; - const verifiedEmails = _verifiedEmails || []; + const verifiedNumbers = useMemo( + () => _verifiedNumbers?.map((number) => number.phoneNumber) || [], + [_verifiedNumbers] + ); + const verifiedEmails = useMemo(() => _verifiedEmails || [], [_verifiedEmails]); const [isAdditionalInputsDialogOpen, setIsAdditionalInputsDialogOpen] = useState(false); const [isTestAgentDialogOpen, setIsTestAgentDialogOpen] = useState(false); const [isWebCallDialogOpen, setIsWebCallDialogOpen] = useState(false); @@ -386,23 +389,29 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { const refEmailSubject = useRef(null); - const getNumberVerificationStatus = () => - !!step && - !!verifiedNumbers.find( - (number: string) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`) - ); + const getNumberVerificationStatus = useCallback( + () => + !!step && + !!verifiedNumbers.find( + (number: string) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`) + ), + [step, verifiedNumbers, form] + ); - const getEmailVerificationStatus = () => - !!step && - !!verifiedEmails.find((email: string) => email === form.getValues(`steps.${step.stepNumber - 1}.sendTo`)); + const getEmailVerificationStatus = useCallback( + () => + !!step && + !!verifiedEmails.find( + (email: string) => email === form.getValues(`steps.${step.stepNumber - 1}.sendTo`) + ), + [step, verifiedEmails, form] + ); const [numberVerified, setNumberVerified] = useState(getNumberVerificationStatus()); const [emailVerified, setEmailVerified] = useState(getEmailVerificationStatus()); - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => setNumberVerified(getNumberVerificationStatus()), [verifiedNumbers.length]); - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => setEmailVerified(getEmailVerificationStatus()), [verifiedEmails.length]); + useEffect(() => setNumberVerified(getNumberVerificationStatus()), [getNumberVerificationStatus]); + useEffect(() => setEmailVerified(getEmailVerificationStatus()), [getEmailVerificationStatus]); const addVariableEmailSubject = (variable: string) => { if (step) { @@ -1011,7 +1020,6 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { {t("send_code")} - {form.formState.errors.steps && form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && (

{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""} @@ -1522,7 +1530,6 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { agentData={agentData} onUpdate={(data) => { updateAgentMutation.mutate({ - //eslint-disable-next-line @typescript-eslint/no-non-null-assertion id: stepAgentId!, teamId: teamId, generalPrompt: data.generalPrompt, diff --git a/packages/features/ee/workflows/lib/schema.ts b/packages/features/ee/workflows/lib/schema.ts index dd0f5da247a4d4..7cde5d47f8a1dc 100644 --- a/packages/features/ee/workflows/lib/schema.ts +++ b/packages/features/ee/workflows/lib/schema.ts @@ -38,8 +38,7 @@ export const formSchema = z.object({ .refine((val) => onlyLettersNumbersSpaces(val)) .optional() .nullable(), - senderName: z.string().nullish(), - agentId: z.string().nullish(), + senderName: z.string().nullable(), }) .array(), selectAll: z.boolean(), diff --git a/packages/features/eventtypes/lib/bookingFieldsManager.ts b/packages/features/eventtypes/lib/bookingFieldsManager.ts index 780005747cd3e5..e52c9e60c4c44b 100644 --- a/packages/features/eventtypes/lib/bookingFieldsManager.ts +++ b/packages/features/eventtypes/lib/bookingFieldsManager.ts @@ -34,13 +34,12 @@ async function getEventType(eventTypeId: EventType["id"]) { throw new Error(`EventType:${eventTypeId} not found`); } + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { profile, ...restEventType } = rawEventType; - const isOrgTeamEvent = !!rawEventType?.teamId && !!profile?.organizationId; - const eventType = { ...restEventType, - bookingFields: getBookingFieldsWithSystemFields({ ...restEventType, isOrgTeamEvent }), + bookingFields: getBookingFieldsWithSystemFields({ ...restEventType }), }; return eventType; } diff --git a/packages/features/eventtypes/lib/getEventTypeById.ts b/packages/features/eventtypes/lib/getEventTypeById.ts index c9b3fae9b6f86f..dde29307862de5 100644 --- a/packages/features/eventtypes/lib/getEventTypeById.ts +++ b/packages/features/eventtypes/lib/getEventTypeById.ts @@ -212,12 +212,11 @@ export const getEventTypeById = async ({ }); } - const isOrgTeamEvent = !!eventType?.teamId && !!eventType.team?.parentId; const eventTypeObject = Object.assign({}, eventType, { users: eventTypeUsers, periodStartDate: eventType.periodStartDate?.toString() ?? null, periodEndDate: eventType.periodEndDate?.toString() ?? null, - bookingFields: getBookingFieldsWithSystemFields({ ...eventType, isOrgTeamEvent }), + bookingFields: getBookingFieldsWithSystemFields({ ...eventType }), }); const isOrgEventType = !!eventTypeObject.team?.parentId; diff --git a/packages/features/eventtypes/lib/getPublicEvent.ts b/packages/features/eventtypes/lib/getPublicEvent.ts index eb579ae791de4d..6bc175ccb36d03 100644 --- a/packages/features/eventtypes/lib/getPublicEvent.ts +++ b/packages/features/eventtypes/lib/getPublicEvent.ts @@ -320,7 +320,11 @@ export const getPublicEvent = async ( return { ...defaultEvent, - bookingFields: getBookingFieldsWithSystemFields({ ...defaultEvent, disableBookingTitle }), + bookingFields: getBookingFieldsWithSystemFields({ + ...defaultEvent, + disableBookingTitle, + workflows: [], + }), // Clears meta data since we don't want to send this in the public api. subsetOfUsers: users.map((user) => ({ ...user, diff --git a/packages/features/form-builder/Components.tsx b/packages/features/form-builder/Components.tsx index 4a1a5bfd6c2ccc..60372f42ed8b07 100644 --- a/packages/features/form-builder/Components.tsx +++ b/packages/features/form-builder/Components.tsx @@ -423,7 +423,7 @@ export const Components: Record = { } return label.search(/^https?:\/\//) !== -1 ? ( - + {label} ) : ( diff --git a/packages/features/form-builder/FormBuilder.tsx b/packages/features/form-builder/FormBuilder.tsx index c9b187db719111..556f2c23ad2158 100644 --- a/packages/features/form-builder/FormBuilder.tsx +++ b/packages/features/form-builder/FormBuilder.tsx @@ -7,6 +7,7 @@ import { ZodError } from "zod"; import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform"; import { Dialog } from "@calcom/features/components/controlled-dialog"; +import { getCountryOptions, getCountryLabel } from "@calcom/lib/countryCodeUtils"; import { getCurrencySymbol } from "@calcom/lib/currencyConversions"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { md } from "@calcom/lib/markdownIt"; @@ -606,7 +607,6 @@ function FieldEditDialog({ const variantsConfig = fieldForm.watch("variantsConfig"); const fieldTypes = Object.values(fieldTypesConfigMap); - const fieldName = fieldForm.getValues("name"); return (

@@ -744,6 +744,39 @@ function FieldEditDialog({ /> )} + {formFieldType === "phone" && ( + ( + ({ ...base, zIndex: 9999 }), + }} + value={ + field.value?.map((code: string) => ({ + label: getCountryLabel(code), + value: code, + })) || [] + } + onChange={(selectedOptions) => { + field.onChange(selectedOptions?.map((option) => option.value) || []); + }} + placeholder={t("select_country_codes")} + /> + )} + /> + )} + {/* Add price field only for fields that support pricing */} {showPriceField && fieldType.supportsPricing && ( & { // Label is optional because radioInput doesn't have a label @@ -251,6 +253,7 @@ export const ComponentForField = ({ readOnly: boolean; noLabel?: boolean; translatedDefaultLabel?: string; + allowedCountryCodes?: string[]; } & ValueProps) => { const fieldType = field.type || "text"; const componentConfig = Components[fieldType]; @@ -281,6 +284,7 @@ export const ComponentForField = ({ readOnly={readOnly} value={value as string} setValue={setValue as (arg: typeof value) => void} + allowedCountryCodes={allowedCountryCodes} /> ); diff --git a/packages/features/instant-meeting/handleInstantMeeting.ts b/packages/features/instant-meeting/handleInstantMeeting.ts index 557216612d2a1a..e2976ac07e977b 100644 --- a/packages/features/instant-meeting/handleInstantMeeting.ts +++ b/packages/features/instant-meeting/handleInstantMeeting.ts @@ -158,10 +158,9 @@ const triggerBrowserNotifications = async (args: { async function _handler(bookingData: CreateInstantBookingData) { let eventType = await getEventTypesFromDB(bookingData.eventTypeId); - const isOrgTeamEvent = !!eventType?.team && !!eventType?.team?.parentId; eventType = { ...eventType, - bookingFields: getBookingFieldsWithSystemFields({ ...eventType, isOrgTeamEvent }), + bookingFields: getBookingFieldsWithSystemFields({ ...eventType }), }; if (!eventType.team?.id) { diff --git a/packages/lib/countryCodeUtils.ts b/packages/lib/countryCodeUtils.ts new file mode 100644 index 00000000000000..2b46ce0c97de15 --- /dev/null +++ b/packages/lib/countryCodeUtils.ts @@ -0,0 +1,77 @@ +import { getCountries, getCountryCallingCode } from "libphonenumber-js"; + +export interface CountryOption { + label: string; + value: string; + dialCode: string; + flag?: string; +} + +const generateCountryData = (): Record => { + const countryData: Record = {}; + const intl = new Intl.DisplayNames(["en"], { type: "region" }); + + const supportedCountries = getCountries(); + + supportedCountries.forEach((code) => { + try { + const dialCode = getCountryCallingCode(code); + const name = intl.of(code) || code; + countryData[code.toLowerCase()] = { name, dialCode }; + } catch (e) { + console.error(`Failed to generate country data for ${code}: ${e}`); + } + }); + + return countryData; +}; + +const countryData = generateCountryData(); + +export const getCountryOptions = (): CountryOption[] => { + return Object.entries(countryData).map(([countryCode, data]) => ({ + label: `${data.name} (+${data.dialCode})`, + value: countryCode.toUpperCase(), + dialCode: data.dialCode, + })); +}; + +export const getCountryLabel = (countryCode: string): string => { + const country = countryData[countryCode.toLowerCase()]; + if (!country) return countryCode; + return `${country.name} (+${country.dialCode})`; +}; + +export const getCountriesWithSameDialCode = (countryCodes: string[]): string[] => { + const dialCodes = new Set(); + const result = new Set(); + + countryCodes.forEach((code) => { + const country = countryData[code.toLowerCase()]; + if (country) { + dialCodes.add(country.dialCode); + } + }); + + Object.entries(countryData).forEach(([countryCode, data]) => { + if (dialCodes.has(data.dialCode)) { + result.add(countryCode.toUpperCase()); + } + }); + + return Array.from(result); +}; + +export const shouldPreventCountryCodeDeletion = (allowedCountryCodes: string[]): boolean => { + if (!allowedCountryCodes || allowedCountryCodes.length === 0) return false; + + const dialCodes = new Set(); + allowedCountryCodes.forEach((code) => { + const country = countryData[code.toLowerCase()]; + if (country) { + dialCodes.add(country.dialCode); + } + }); + + return dialCodes.size === 1; +}; diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index afddcb2146b3da..5e28d7da75c3f7 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -948,6 +948,7 @@ export const fieldSchema = baseFieldSchema.merge( ) .optional(), disableOnPrefill: z.boolean().default(false).optional(), + allowedCountryCodes: z.array(z.string()).optional(), }) );