-
Notifications
You must be signed in to change notification settings - Fork 12k
feat: add country code filtering for SMS workflows #23021
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
02e13b5
d4e5e39
428e004
8e5f5d9
8597a4d
b18f24f
a5c90c6
93c686b
182c7d4
71353df
29673a5
31b4c02
ffe86c6
45f5e7e
605075b
2ea935e
e7081eb
85b55de
3a308c1
095f502
e18e579
7a372ea
64bdb77
746cbd2
abba4e0
3a070a5
434098d
833787a
5aa4fd9
67973dd
9f73b9f
dbf5fe7
8f28c40
ea32e36
55f388b
dee6e56
4eeb9c8
662f835
a9e0938
df89000
f886ee8
882a075
056d830
97c5c82
aedab18
8fcf65a
ea7fe5e
6ae2e13
8edef4c
9ab2a9e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -47,6 +47,7 @@ export const getSmsReminderNumberSource = ({ | |||||
| }: { | ||||||
| workflowId: Workflow["id"]; | ||||||
| isSmsReminderNumberRequired: boolean; | ||||||
| allowedCountryCodes?: string[]; | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This is not used |
||||||
| }) => ({ | ||||||
| id: `${workflowId}`, | ||||||
| type: "workflow", | ||||||
|
|
@@ -84,15 +85,13 @@ export const getAIAgentCallPhoneNumberSource = ({ | |||||
| export const getBookingFieldsWithSystemFields = ({ | ||||||
| bookingFields, | ||||||
| disableGuests, | ||||||
| isOrgTeamEvent = false, | ||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing an unused variable here. |
||||||
| disableBookingTitle, | ||||||
| customInputs, | ||||||
| metadata, | ||||||
| workflows, | ||||||
| }: { | ||||||
| bookingFields: Fields | EventType["bookingFields"]; | ||||||
| disableGuests: boolean; | ||||||
| isOrgTeamEvent?: boolean; | ||||||
| disableBookingTitle?: boolean; | ||||||
| customInputs: EventTypeCustomInput[] | z.infer<typeof customInputSchema>[]; | ||||||
| metadata: EventType["metadata"] | z.infer<typeof EventTypeMetaDataSchema>; | ||||||
|
|
@@ -107,7 +106,6 @@ export const getBookingFieldsWithSystemFields = ({ | |||||
| return ensureBookingInputsHaveSystemFields({ | ||||||
| bookingFields: parsedBookingFields, | ||||||
| disableGuests, | ||||||
| isOrgTeamEvent, | ||||||
| disableBookingTitle, | ||||||
| additionalNotesRequired: parsedMetaData?.additionalNotesRequired || false, | ||||||
| customInputs: parsedCustomInputs, | ||||||
|
|
@@ -118,15 +116,13 @@ export const getBookingFieldsWithSystemFields = ({ | |||||
| export const ensureBookingInputsHaveSystemFields = ({ | ||||||
| bookingFields, | ||||||
| disableGuests, | ||||||
| isOrgTeamEvent, | ||||||
| disableBookingTitle, | ||||||
| additionalNotesRequired, | ||||||
| customInputs, | ||||||
| workflows, | ||||||
| }: { | ||||||
| bookingFields: Fields; | ||||||
| disableGuests: boolean; | ||||||
| isOrgTeamEvent: boolean; | ||||||
| disableBookingTitle?: boolean; | ||||||
| additionalNotesRequired: boolean; | ||||||
| customInputs: z.infer<typeof customInputSchema>[]; | ||||||
|
|
@@ -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, | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,11 @@ | ||
| 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"; | ||
| import { dbReadResponseSchema, fieldTypesSchemaMap } from "@calcom/features/form-builder/schema"; | ||
| 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<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">) | null; | ||
| type CommonParams = { bookingFields: BookingFields; view: View }; | ||
|
|
@@ -103,15 +102,41 @@ function preprocess<T extends z.ZodType>({ | |
| }; | ||
| 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); | ||
| } | ||
joeauyeung marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| newResponses[field.name] = parsedValue; | ||
| } else if (field.type === "phone") { | ||
| newResponses[field.name] = ensureValidPhoneNumber(value); | ||
| // Auto-prepend country code for single country restrictions | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| 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 { | ||
joeauyeung marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| newResponses[field.name] = value; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ export type PhoneInputProps = { | |
| disabled?: boolean; | ||
| onChange: (value: string) => void; | ||
| defaultCountry?: string; | ||
| allowedCountryCodes?: string[]; | ||
joeauyeung marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 ( | ||
| <BasePhoneInputWeb name={name} className={className} onChange={onChange} value={value} {...rest} /> | ||
| <BasePhoneInputWeb | ||
| name={name} | ||
| className={className} | ||
| onChange={onChange} | ||
| value={value} | ||
| allowedCountryCodes={allowedCountryCodes} | ||
| {...rest} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| 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; | ||
|
|
||
joeauyeung marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return ( | ||
| <PhoneInput | ||
| {...rest} | ||
| value={value ? value.trim().replace(/^\+?/, "+") : undefined} | ||
| enableSearch | ||
| disableSearchIcon | ||
| country={defaultCountry} | ||
| country={effectiveDefaultCountry} | ||
| onlyCountries={onlyCountries && onlyCountries.length > 0 ? onlyCountries : undefined} | ||
| disableCountryCode={singleCountry} | ||
| inputProps={{ | ||
|
Comment on lines
83
to
86
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 🧩 Analysis chainConfirm react-phone-input-2 prop name: disableCountryCode vs countryCodeEditable. If the library expects countryCodeEditable={false} (not disableCountryCode), the current prop will be ignored. Also applies to: 148-149 🌐 Web query: 💡 Result: Short answer: Use countryCodeEditable={false} to lock the dial-code prefix. The component also exposes disableCountryCode (which tries to keep the dial code out of the input value), but it does not prevent the dial code/auto-guessing behavior reliably — users have reported unexpected behavior and issues. See the prop docs and related GitHub issues. [1][2] Sources:
Use countryCodeEditable={false} instead of disableCountryCode – disableCountryCode only omits the dial code from the value and doesn’t reliably lock the prefix. Update the props in packages/features/components/phone-input/PhoneInput.tsx (lines 86-89, 148-149). 🤖 Prompt for AI Agents |
||
| name, | ||
| required: rest.required, | ||
|
|
@@ -101,19 +120,29 @@ function BasePhoneInputWeb({ | |
| className = "", | ||
| onChange, | ||
| value, | ||
| allowedCountryCodes, | ||
| inputStyle, | ||
| flagButtonStyle, | ||
| ...rest | ||
| }: Omit<PhoneInputProps, "defaultCountry">) { | ||
| 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 ( | ||
| <PhoneInput | ||
| {...rest} | ||
| value={value ? value.trim().replace(/^\+?/, "+") : undefined} | ||
| country={value ? undefined : defaultCountry} | ||
| country={value ? undefined : effectiveDefaultCountry} | ||
| enableSearch | ||
| disableSearchIcon | ||
| onlyCountries={onlyCountries && onlyCountries.length > 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] | ||
| ); | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Platform changes are eslint fixes. Since we removed the unused param from
getResponseEventType