Skip to content

Comments

feat: add country code filtering for SMS workflows#23021

Closed
joeauyeung wants to merge 50 commits intomainfrom
devin/sms-country-code-filtering-1754928743
Closed

feat: add country code filtering for SMS workflows#23021
joeauyeung wants to merge 50 commits intomainfrom
devin/sms-country-code-filtering-1754928743

Conversation

@joeauyeung
Copy link
Contributor

@joeauyeung joeauyeung commented Aug 11, 2025

What does this PR do?

This PR implements country code filtering for SMS workflows in Cal.com, allowing users to restrict SMS notifications to specific countries. It also includes performance optimizations for country code generation.

Key Features:

  • 📍 Country Code Selection: Added UI dropdown to select allowed countries for SMS workflows
  • 🚫 SMS Filtering: SMS messages are only sent to phone numbers from selected countries
  • 💡 User Clarification: Added help text explaining that empty selection allows all countries
  • Performance Optimization: Refactored country data generation from 676-iteration brute force to single getCountries() call

Database Changes:

  • Added allowedCountryCodes field to WorkflowStep model (defaults to empty array = all countries allowed)

Visual Demo

https://www.loom.com/share/090dd55a92764e95bdc0bbdaa3145611

Technical Implementation

Frontend Changes:

  • New country selection dropdown in workflow step configuration
  • Phone input components now respect country code restrictions
  • Added help text with info icon following existing UI patterns
  • Form validation and type updates throughout the codebase

Backend Changes:

  • Phone number country validation before SMS sending
  • Booking field system updated to handle country code restrictions
  • Country code intersection logic for multiple SMS workflows
  • Proper error handling and logging for invalid phone numbers

Performance Improvements:

  • Replaced nested loops (26×26=676 iterations) with direct getCountries() call
  • Reduced function calls from ~876 to 1 for country data generation
  • Maintained identical functionality and caching behavior

How should this be tested?

Environment Setup:

  • Standard Cal.com development environment
  • Access to workflow configuration pages

Test Scenarios:

  1. Basic Functionality:

    • Create new SMS workflow → verify country dropdown appears
    • Select specific countries → save and verify selection persists
    • Leave empty → verify help text indicates all countries allowed
  2. SMS Filtering:

    • Configure workflow with country restrictions
    • Test phone numbers from allowed/blocked countries
    • Verify SMS only sent to allowed countries
  3. Backward Compatibility:

    • Existing workflows should continue working unchanged
    • Empty allowedCountryCodes should allow all countries
  4. Edge Cases:

    • Invalid phone number formats
    • Multiple SMS workflows with different country restrictions
    • Countries with same dial codes

Mandatory Tasks

  • I have self-reviewed the code ✅
  • Documentation updates: N/A - internal workflow feature
  • Automated tests: Existing SMS workflow tests cover core functionality

Important Review Areas

⚠️ Critical Items for Review:

  • Database Migration: Verify allowedCountryCodes field migration works correctly
  • Type Safety: Complex booking field type changes - check for TypeScript errors
  • Backward Compatibility: Ensure existing workflows without country codes continue working
  • Phone Parsing: Error handling in smsReminderManager.ts for invalid phone numbers
  • Performance: Country code caching and multiple function call sites

Files Requiring Extra Attention:

  • packages/features/bookings/lib/getBookingFields.ts - Complex type manipulations
  • packages/features/ee/workflows/lib/reminders/smsReminderManager.ts - Phone validation logic
  • packages/prisma/schema.prisma - Database schema changes

Link to Devin run: https://app.devin.ai/sessions/5d47ef9c555c4aaca9c657e6cf713f1e
Requested by: @joeauyeung

- Add allowedCountryCodes field to WorkflowStep model in database schema
- Update workflow step configuration UI to allow country code selection for SMS actions
- Implement SMS validation to check phone numbers against allowed country codes
- Modify PhoneInput component to support country code restrictions and prevent deletion when only one code allowed
- Update booking form integration to pass country code restrictions from workflows
- Add translation strings for new UI elements
- Update all TypeScript types to support new allowedCountryCodes field

This allows users to restrict SMS workflows to specific country codes and prevents bookers from deleting country code prefixes when only one is allowed (e.g., +1 for US/Canada).

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 11, 2025

Walkthrough

This change removes the isOrgTeamEvent flag across API, services, and booking field builders, updating method signatures and call sites accordingly. Booking field generation now relies solely on eventType data. It introduces allowedCountryCodes support end-to-end: form builder fields, phone input components (web and mobile), and booking response schema (auto-prepending calling codes when a single country is allowed). A new countryCodeUtils module provides country options and labels. Public group events now pass workflows: [] into booking field generation. Misc updates include translations for country code restrictions, memoization in WorkflowStepContainer, a schema tweak (senderName nullable; agentId removed), and a security rel attribute.

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly summarizes the main enhancement by specifying the addition of country code filtering for SMS workflows, matching the core change in the pull request and using clear, descriptive language without extraneous details.
Description Check ✅ Passed The description thoroughly outlines the feature, implementation details, backend and frontend changes, migration steps, testing guidance, and edge cases, directly reflecting the pull request’s content and objectives.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch devin/sms-country-code-filtering-1754928743

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ea7fe5e and 9ab2a9e.

📒 Files selected for processing (1)
  • packages/features/ee/workflows/lib/schema.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/features/ee/workflows/lib/schema.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@keithwillcode keithwillcode added core area: core, team members only enterprise area: enterprise, audit log, organisation, SAML, SSO labels Aug 11, 2025
@github-actions
Copy link
Contributor

This PR is being marked as stale due to inactivity.

@github-actions github-actions bot added the Stale label Aug 26, 2025
…uration

- Replace hardcoded 20 countries with dynamic generation of all 245 supported countries
- Use libphonenumber-js.isSupportedCountry() and getCountryCallingCode() for consistency
- Use Intl.DisplayNames for proper country name localization
- Ensures workflow country selection matches PhoneInput component options exactly

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
@vercel
Copy link

vercel bot commented Sep 8, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
cal Ignored Ignored Sep 25, 2025 4:51pm
cal-eu Ignored Ignored Sep 25, 2025 4:51pm

devin-ai-integration bot and others added 2 commits September 8, 2025 19:36
- Update WorkflowStepContainer to show country code UI only for SMS_ATTENDEE
- Add allowedCountryCodes field to FormValues type and form schema
- Ensures country code filtering is specific to attendee SMS workflows

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
@github-actions github-actions bot added ❗️ migrations contains migration files ❗️ .env changes contains changes to env variables labels Sep 9, 2025
Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/features/components/phone-input/PhoneInput.tsx (2)

80-117: Enforce allowed countries on typed input (cannot bypass by pasting another E.164).

Users can still enter numbers from disallowed countries; add client-side validation with libphonenumber-js.

   return (
     <PhoneInput
       {...rest}
       value={value ? value.trim().replace(/^\+?/, "+") : undefined}
       enableSearch
       disableSearchIcon
+      isValid={(val: string) => {
+        if (!allowedCountryCodes || allowedCountryCodes.length === 0) return true;
+        try {
+          const parsed = parsePhoneNumberFromString(`+${val}`);
+          if (!parsed?.country) return true; // allow partial input
+          const allowed = new Set(allowedCountryCodes.map((c) => c.toUpperCase()));
+          return allowed.has(parsed.country);
+        } catch {
+          return true;
+        }
+      }}
       country={effectiveDefaultCountry}
       onlyCountries={onlyCountries}
       disableCountryCode={disableCountryCode}

Add import:

-import { isSupportedCountry } from "libphonenumber-js";
+import { isSupportedCountry, parsePhoneNumberFromString } from "libphonenumber-js";

142-179: Add isValid here too to prevent bypass on web.

Keep behavior consistent across both render paths.

   return (
     <PhoneInput
       {...rest}
       value={value ? value.trim().replace(/^\+?/, "+") : undefined}
       country={value ? undefined : effectiveDefaultCountry}
       enableSearch
       disableSearchIcon
+      isValid={(val: string) => {
+        if (!allowedCountryCodes || allowedCountryCodes.length === 0) return true;
+        try {
+          const parsed = parsePhoneNumberFromString(`+${val}`);
+          if (!parsed?.country) return true;
+          const allowed = new Set(allowedCountryCodes.map((c) => c.toUpperCase()));
+          return allowed.has(parsed.country);
+        } catch {
+          return true;
+        }
+      }}
       onlyCountries={onlyCountries}
       disableCountryCode={disableCountryCode}
🧹 Nitpick comments (3)
packages/features/components/phone-input/PhoneInput.tsx (3)

23-24: Clarify prop semantics (empty list = unrestricted; ISO codes).

Document expected format and behavior to avoid misuse downstream.

 export type PhoneInputProps = {
@@
-  allowedCountryCodes?: string[];
-  preventCountryCodeDeletion?: boolean;
+  /** ISO 3166-1 alpha‑2 codes (e.g., "US", "AT"). undefined or [] = no restriction. */
+  allowedCountryCodes?: string[];
+  /** When true and all allowed countries share the same dial code, lock the prefix (non‑editable). */
+  preventCountryCodeDeletion?: boolean;

136-139: Prefer viewer’s default when allowed; otherwise fallback to first allowed.

Improves UX: keep user’s locale when it’s within the restriction.

-  const effectiveDefaultCountry = allowedCountryCodes?.length
-    ? allowedCountryCodes[0].toLowerCase()
-    : defaultCountry;
+  const effectiveDefaultCountry =
+    allowedCountryCodes && allowedCountryCodes.length > 0
+      ? (allowedCountryCodes.includes(defaultCountry.toUpperCase())
+          ? defaultCountry
+          : allowedCountryCodes[0].toLowerCase())
+      : defaultCountry;

210-210: Prefer named export over default export.

Aligns with tree‑shaking and refactorability guidelines.

-export default BasePhoneInput;
+export { BasePhoneInput };
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 55f388b and 662f835.

📒 Files selected for processing (2)
  • packages/features/components/phone-input/PhoneInput.tsx (5 hunks)
  • packages/features/form-builder/FormBuilder.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/features/form-builder/FormBuilder.tsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Always use t() for text localization in frontend code; direct text embedding should trigger a warning

Files:

  • packages/features/components/phone-input/PhoneInput.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/features/components/phone-input/PhoneInput.tsx
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • packages/features/components/phone-input/PhoneInput.tsx
🧠 Learnings (2)
📓 Common learnings
Learnt from: supalarry
PR: calcom/cal.com#23364
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/internal-to-api.spec.ts:295-296
Timestamp: 2025-08-27T13:32:46.887Z
Learning: In calcom/cal.com, when transforming booking fields from internal to API format, tests in organizations-event-types.e2e-spec.ts already expect name field label and placeholder to be empty strings ("") rather than undefined. PR changes that set these to explicit empty strings are typically fixing implementation to match existing test expectations rather than breaking changes.
📚 Learning: 2025-08-08T10:26:13.362Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22919
File: packages/features/calAIPhone/providers/retellAI/services/PhoneNumberService.ts:212-220
Timestamp: 2025-08-08T10:26:13.362Z
Learning: In calcom/cal.com PR #22919, packages/features/calAIPhone/providers/retellAI/services/PhoneNumberService.ts should include the phone number in client-facing HttpError messages (e.g., in updatePhoneNumber/getPhoneNumber catch blocks). Do not suggest redacting the phone number from these errors unless requirements change (per maintainer: Udit-takkar).

Applied to files:

  • packages/features/components/phone-input/PhoneInput.tsx
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Install dependencies / Yarn install & cache
🔇 Additional comments (4)
packages/features/components/phone-input/PhoneInput.tsx (4)

145-145: Good: don’t override country when a value exists.

Avoids flicker and preserves the parsed country from the existing value.


198-202: LGTM: sensible default-country fallback.

Uses server hint when supported; otherwise falls back to navigator region or “us”.


72-79: Bug: onlyCountries=[] disables all countries; treat [] as “no restriction”. Also lock prefix when all allowed share dial code.

Current logic breaks the picker when allowedCountryCodes is empty and doesn’t handle shared dial codes (+1).

Apply this diff:

-  const onlyCountries = allowedCountryCodes?.map((code) => code.toLowerCase());
-  const disableCountryCode = preventCountryCodeDeletion && allowedCountryCodes?.length === 1;
+  const onlyCountries =
+    allowedCountryCodes && allowedCountryCodes.length > 0
+      ? allowedCountryCodes.map((code) => code.toLowerCase())
+      : undefined;
+  const disableCountryCode =
+    !!preventCountryCodeDeletion &&
+    !!allowedCountryCodes &&
+    allowedCountryCodes.length > 0 &&
+    (() => {
+      try {
+        const first = getCountryCallingCode(allowedCountryCodes[0] as any);
+        return allowedCountryCodes.every(
+          (c) => getCountryCallingCode(c as any) === first
+        );
+      } catch {
+        return false;
+      }
+    })();

And add the import:

-import { isSupportedCountry } from "libphonenumber-js";
+import { isSupportedCountry, getCountryCallingCode } from "libphonenumber-js";

Also mirror the same change in the web path (Lines 133-140).


126-131: Same fixes needed in web path (onlyCountries empty → undefined; shared dial code lock).

Mirror the bug fix and locking logic from the platform path here.

-  const onlyCountries = allowedCountryCodes?.map((code) => code.toLowerCase());
-  const disableCountryCode = preventCountryCodeDeletion && allowedCountryCodes?.length === 1;
+  const onlyCountries =
+    allowedCountryCodes && allowedCountryCodes.length > 0
+      ? allowedCountryCodes.map((code) => code.toLowerCase())
+      : undefined;
+  const disableCountryCode =
+    !!preventCountryCodeDeletion &&
+    !!allowedCountryCodes &&
+    allowedCountryCodes.length > 0 &&
+    (() => {
+      try {
+        const first = getCountryCallingCode(allowedCountryCodes[0] as any);
+        return allowedCountryCodes.every(
+          (c) => getCountryCallingCode(c as any) === first
+        );
+      } catch {
+        return false;
+      }
+    })();

Also applies to: 133-140

Comment on lines 86 to 89
country={effectiveDefaultCountry}
onlyCountries={onlyCountries}
disableCountryCode={disableCountryCode}
inputProps={{
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

🧩 Analysis chain

Confirm 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:

Does react-phone-input-2 support a `disableCountryCode` prop, or should `countryCodeEditable={false}` be used to lock the dial code prefix?

💡 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:

  • react-phone-input-2 props documentation (shows disableCountryCode and countryCodeEditable). [1]
  • GitHub issues discussing disableCountryCode bugs and unexpected country-guess behavior. [2]

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
In packages/features/components/phone-input/PhoneInput.tsx around lines 86-89
and 148-149, the component currently passes disableCountryCode which only omits
the dial code and doesn’t lock the prefix; replace disableCountryCode with
countryCodeEditable={false} in the props passed to the phone input so the
country dial code is rendered but not editable, and ensure any related logic or
prop typings are updated accordingly to accept countryCodeEditable.

devin-ai-integration bot and others added 2 commits September 24, 2025 19:35
- Fix phone validation failure when allowedCountryCodes has single country
- Auto-prepend country code in onChange when disableCountryCode is true
- Maintain UI behavior (hide country code) while fixing form submission
- Apply fix to both BasePhoneInput and BasePhoneInputWeb functions
- Remove allowedCountryCodes references from workflow update handler
- Use proper CountryCode type from libphonenumber-js
- Fix ESLint warnings with proper disable comments

Fixes booking submission failures when phone fields have single allowed
country code by ensuring E.164 format validation passes while keeping
the clean UI experience for users.

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
packages/features/components/phone-input/PhoneInput.tsx (5)

23-24: Clarify prop semantics inline

Document that empty/undefined means “no restriction,” and the expected format.

-  allowedCountryCodes?: string[];
-  preventCountryCodeDeletion?: boolean;
+  /** ISO-3166-1 alpha-2 codes (e.g., "US"). Empty/undefined = no restriction */
+  allowedCountryCodes?: string[];
+  /** When true, lock the dial-code prefix if it can be deterministically derived */
+  preventCountryCodeDeletion?: boolean;

75-79: Derive default from normalized list

Prefer onlyCountries[0] (already lowercased/validated) to raw allowedCountryCodes.

-  const effectiveDefaultCountry = allowedCountryCodes?.length
-    ? allowedCountryCodes[0].toLowerCase()
-    : defaultCountry;
+  const effectiveDefaultCountry = onlyCountries?.[0] ?? defaultCountry;

136-139: Derive default from normalized list (web path)

Use the normalized list for consistency.

-  const effectiveDefaultCountry = allowedCountryCodes?.length
-    ? allowedCountryCodes[0].toLowerCase()
-    : defaultCountry;
+  const effectiveDefaultCountry = onlyCountries?.[0] ?? defaultCountry;

198-202: Harden navigator fallback (unsupported or UK alias)

Normalize navigator code and ensure it’s supported; fall back to “us”.

-      } else {
-        setDefaultCountry(navigator.language.split("-")[1]?.toLowerCase() || "us");
-      }
+      } else {
+        const nav = navigator.language.split("-")[1]?.toLowerCase();
+        const normalized = nav === "uk" ? "gb" : nav;
+        setDefaultCountry(
+          normalized && isSupportedCountry(normalized.toUpperCase()) ? normalized : "us"
+        );
+      }

210-210: Prefer named exports for components

Switching from default to named export improves tree-shaking and refactors. This is a broad change; consider as a follow-up.

If you proceed, update import sites:

-export default BasePhoneInput;
+export { BasePhoneInput as PhoneInput };

Then replace default imports with:
import { PhoneInput } from "packages/features/components/phone-input/PhoneInput";

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a9e0938 and df89000.

📒 Files selected for processing (2)
  • packages/features/components/phone-input/PhoneInput.tsx (5 hunks)
  • packages/trpc/server/routers/viewer/workflows/update.handler.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/trpc/server/routers/viewer/workflows/update.handler.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Always use t() for text localization in frontend code; direct text embedding should trigger a warning

Files:

  • packages/features/components/phone-input/PhoneInput.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/features/components/phone-input/PhoneInput.tsx
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • packages/features/components/phone-input/PhoneInput.tsx
🧠 Learnings (2)
📓 Common learnings
Learnt from: supalarry
PR: calcom/cal.com#23364
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/internal-to-api.spec.ts:295-296
Timestamp: 2025-08-27T13:32:46.887Z
Learning: In calcom/cal.com, when transforming booking fields from internal to API format, tests in organizations-event-types.e2e-spec.ts already expect name field label and placeholder to be empty strings ("") rather than undefined. PR changes that set these to explicit empty strings are typically fixing implementation to match existing test expectations rather than breaking changes.
Learnt from: supalarry
PR: calcom/cal.com#23217
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts:93-94
Timestamp: 2025-08-21T13:44:06.805Z
Learning: In apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts, repository functions that use explicit Prisma select clauses (like getEventTypeWithSeats) are used for specific purposes and don't need to include all EventType fields like bookingRequiresAuthentication. These methods don't feed into the general OutputEventTypesService_2024_06_14 flow.
📚 Learning: 2025-08-08T10:26:13.362Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22919
File: packages/features/calAIPhone/providers/retellAI/services/PhoneNumberService.ts:212-220
Timestamp: 2025-08-08T10:26:13.362Z
Learning: In calcom/cal.com PR #22919, packages/features/calAIPhone/providers/retellAI/services/PhoneNumberService.ts should include the phone number in client-facing HttpError messages (e.g., in updatePhoneNumber/getPhoneNumber catch blocks). Do not suggest redacting the phone number from these errors unless requirements change (per maintainer: Udit-takkar).

Applied to files:

  • packages/features/components/phone-input/PhoneInput.tsx
🔇 Additional comments (6)
packages/features/components/phone-input/PhoneInput.tsx (6)

60-68: Props plumb-through LGTM
Forwarding the new props into BasePhoneInputWeb looks correct.


72-79: Optional: lock dial code when all allowed share the same calling code

If multiple allowed countries share a dial code (e.g., US/CA = +1), you can still lock the prefix. Requires getCountryCallingCode.

+import { getCountryCallingCode } from "libphonenumber-js";
@@
-  const disableCountryCode =
-    !!preventCountryCodeDeletion && !!allowedCountryCodes && allowedCountryCodes.length === 1;
+  const disableCountryCode =
+    !!preventCountryCodeDeletion &&
+    !!allowedCountryCodes &&
+    allowedCountryCodes.length > 0 &&
+    (() => {
+      try {
+        const first = getCountryCallingCode(allowedCountryCodes[0] as any);
+        return allowedCountryCodes.every(
+          (c) => getCountryCallingCode(c as any) === first
+        );
+      } catch {
+        return false;
+      }
+    })();

Also applies to: 126-139


72-74: Bug: [] should mean “no restriction”, not “disable all countries”

Passing onlyCountries=[] blocks selection entirely. Use undefined when the list is empty.

-  const onlyCountries = allowedCountryCodes?.map((code) => code.toLowerCase());
-  const disableCountryCode = preventCountryCodeDeletion && allowedCountryCodes?.length === 1;
+  const onlyCountries =
+    allowedCountryCodes && allowedCountryCodes.length > 0
+      ? allowedCountryCodes.map((code) => code.toLowerCase())
+      : undefined;
+  const disableCountryCode =
+    !!preventCountryCodeDeletion && !!allowedCountryCodes && allowedCountryCodes.length === 1;

86-89: Lock dial code with countryCodeEditable, and don’t override once value exists

Use countryCodeEditable to prevent entering disallowed dial codes; avoid forcing country when value is present. This addresses the UX bug reported by Carina.

-      country={effectiveDefaultCountry}
-      onlyCountries={onlyCountries}
-      disableCountryCode={disableCountryCode}
+      country={value ? undefined : effectiveDefaultCountry}
+      onlyCountries={onlyCountries}
+      countryCodeEditable={disableCountryCode ? false : undefined}

126-135: Repeat fix: [] → undefined and robust boolean

Mirror the non-web path: undefined means unrestricted; tighten boolean checks.

-  const onlyCountries = allowedCountryCodes?.map((code) => code.toLowerCase());
-  const disableCountryCode = preventCountryCodeDeletion && allowedCountryCodes?.length === 1;
+  const onlyCountries =
+    allowedCountryCodes && allowedCountryCodes.length > 0
+      ? allowedCountryCodes.map((code) => code.toLowerCase())
+      : undefined;
+  const disableCountryCode =
+    !!preventCountryCodeDeletion && !!allowedCountryCodes && allowedCountryCodes.length === 1;

145-149: Lock dial code with countryCodeEditable (web path)

Use the correct prop to actually prevent dial-code edits; keeps user from entering disallowed countries.

-      country={value ? undefined : effectiveDefaultCountry}
+      country={value ? undefined : effectiveDefaultCountry}
       enableSearch
       disableSearchIcon
-      onlyCountries={onlyCountries}
-      disableCountryCode={disableCountryCode}
+      onlyCountries={onlyCountries}
+      countryCodeEditable={disableCountryCode ? false : undefined}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (5)
packages/features/bookings/lib/getBookingResponsesSchema.ts (5)

1-1: Import parsePhoneNumberFromString to enable country enforcement

You’ll need this for server-side enforcement of allowedCountryCodes in validation.

-import { isValidPhoneNumber, getCountryCallingCode, type CountryCode } from "libphonenumber-js";
+import { isValidPhoneNumber, getCountryCallingCode, parsePhoneNumberFromString, type CountryCode } from "libphonenumber-js";

303-314: Enforce allowedCountryCodes at validation time (not only via UI)

Add a server-side check so numbers must belong to the allowed set (covers multi-country restrictions and UI bypass).

         if (bookingField.type === "phone") {
           // Determine if the phone field needs validation
           const needsValidation = isRequired || (value && value.trim() !== "");
 
           // Validate phone number if the field is not hidden and requires validation
           if (!bookingField.hidden && needsValidation) {
             if (!(await phoneSchema.safeParseAsync(value)).success) {
               ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("invalid_number") });
             }
+            // Enforce allowed country codes if configured
+            if (
+              Array.isArray(bookingField.allowedCountryCodes) &&
+              bookingField.allowedCountryCodes.length > 0
+            ) {
+              const parsed = parsePhoneNumberFromString(value);
+              const allowed = bookingField.allowedCountryCodes.map((c) => c.toUpperCase());
+              const country = parsed?.country?.toUpperCase();
+              if (country && !allowed.includes(country)) {
+                ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("invalid_number") });
+              }
+            }
           }
           continue;
         }

337-344: Also enforce allowedCountryCodes for radioInput phone option values

Parity with the plain phone field so radio-driven phone inputs can’t bypass the restriction.

               if (typeOfOptionInput === "phone") {
                 if (!(await phoneSchema.safeParseAsync(optionValue)).success) {
                   ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("invalid_number") });
                 }
+                // Enforce allowed country codes if configured on the parent field
+                if (
+                  Array.isArray(bookingField.allowedCountryCodes) &&
+                  bookingField.allowedCountryCodes.length > 0
+                ) {
+                  const parsed = parsePhoneNumberFromString(optionValue);
+                  const allowed = bookingField.allowedCountryCodes.map((c) => c.toUpperCase());
+                  const country = parsed?.country?.toUpperCase();
+                  if (country && !allowed.includes(country)) {
+                    ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("invalid_number") });
+                  }
+                }
               }

111-124: Reduce duplication: extract a small helper for “single allowed country” auto-prepend

Keeps logic consistent between radioInput and plain phone fields.

Example helper (place near ensureValidPhoneNumber):

function prependDialCodeIfSingleAllowedCountry(
  raw: string,
  allowed?: string[]
): string {
  if (!raw || !allowed?.length || allowed.length !== 1) return raw;
  const cc = allowed[0]?.toUpperCase();
  try {
    const dial = getCountryCallingCode(cc as CountryCode);
    return raw.startsWith("+") ? raw : `+${dial}${raw}`;
  } catch {
    return raw;
  }
}

Then replace both blocks with:

  • radioInput: phoneValue = prependDialCodeIfSingleAllowedCountry(phoneValue, field.allowedCountryCodes);
  • phone: phoneValue = prependDialCodeIfSingleAllowedCountry(value, field.allowedCountryCodes);

Also applies to: 127-140


33-36: Prefer named exports over default

Aligns with internal guidelines for clearer imports and better tree-shaking. Can be a follow-up, not blocking this PR.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between df89000 and 056d830.

📒 Files selected for processing (3)
  • packages/features/bookings/lib/getBookingResponsesSchema.ts (2 hunks)
  • packages/features/components/phone-input/PhoneInput.tsx (5 hunks)
  • packages/features/form-builder/Components.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/features/components/phone-input/PhoneInput.tsx
  • packages/features/form-builder/Components.tsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/features/bookings/lib/getBookingResponsesSchema.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/features/bookings/lib/getBookingResponsesSchema.ts
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • packages/features/bookings/lib/getBookingResponsesSchema.ts
🧠 Learnings (3)
📓 Common learnings
Learnt from: supalarry
PR: calcom/cal.com#23364
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/internal-to-api.spec.ts:295-296
Timestamp: 2025-08-27T13:32:46.887Z
Learning: In calcom/cal.com, when transforming booking fields from internal to API format, tests in organizations-event-types.e2e-spec.ts already expect name field label and placeholder to be empty strings ("") rather than undefined. PR changes that set these to explicit empty strings are typically fixing implementation to match existing test expectations rather than breaking changes.
Learnt from: supalarry
PR: calcom/cal.com#23217
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts:93-94
Timestamp: 2025-08-21T13:44:06.805Z
Learning: In apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts, repository functions that use explicit Prisma select clauses (like getEventTypeWithSeats) are used for specific purposes and don't need to include all EventType fields like bookingRequiresAuthentication. These methods don't feed into the general OutputEventTypesService_2024_06_14 flow.
📚 Learning: 2025-08-08T10:26:13.362Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22919
File: packages/features/calAIPhone/providers/retellAI/services/PhoneNumberService.ts:212-220
Timestamp: 2025-08-08T10:26:13.362Z
Learning: In calcom/cal.com PR #22919, packages/features/calAIPhone/providers/retellAI/services/PhoneNumberService.ts should include the phone number in client-facing HttpError messages (e.g., in updatePhoneNumber/getPhoneNumber catch blocks). Do not suggest redacting the phone number from these errors unless requirements change (per maintainer: Udit-takkar).

Applied to files:

  • packages/features/bookings/lib/getBookingResponsesSchema.ts
📚 Learning: 2025-08-27T13:32:46.887Z
Learnt from: supalarry
PR: calcom/cal.com#23364
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/internal-to-api.spec.ts:295-296
Timestamp: 2025-08-27T13:32:46.887Z
Learning: In calcom/cal.com, when transforming booking fields from internal to API format, tests in organizations-event-types.e2e-spec.ts already expect name field label and placeholder to be empty strings ("") rather than undefined. PR changes that set these to explicit empty strings are typically fixing implementation to match existing test expectations rather than breaking changes.

Applied to files:

  • packages/features/bookings/lib/getBookingResponsesSchema.ts
🔇 Additional comments (1)
packages/features/bookings/lib/getBookingResponsesSchema.ts (1)

105-107: LGTM on safe JSON parse

Graceful handling is good; avoids crashing preprocess on malformed radioInput payloads.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
packages/features/components/phone-input/PhoneInput.tsx (5)

23-26: Type allowedCountryCodes precisely (CountryCode[]) and document semantics

Use the library’s CountryCode type to prevent invalid inputs and make intent clear (empty array = no restriction).

-import type { CSSProperties } from "react";
+import type { CSSProperties } from "react";
+import type { CountryCode } from "libphonenumber-js/core";
@@
-  allowedCountryCodes?: string[];
+  allowedCountryCodes?: CountryCode[];

Note: You’ll need the type-only import above.


69-76: Normalize/validate allowedCountryCodes and precompute dial-code locking

Filter out unsupported codes and compute a lockDialCode flag when all allowed countries share the same calling code (e.g., US/CA). This improves robustness and UX.

-  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;
+  const normalizedAllowed =
+    Array.isArray(allowedCountryCodes) && allowedCountryCodes.length > 0
+      ? allowedCountryCodes
+          .map((c) => c?.toUpperCase())
+          .filter((c): c is string => !!c && isSupportedCountry(c as any))
+      : undefined;
+
+  const onlyCountries = normalizedAllowed?.map((c) => c.toLowerCase());
+
+  const effectiveDefaultCountry =
+    onlyCountries && onlyCountries.length > 0 ? onlyCountries[0] : defaultCountry;
+
+  // Lock dial-code if all allowed countries share the same calling code
+  const lockDialCode =
+    !!normalizedAllowed &&
+    normalizedAllowed.length > 0 &&
+    (() => {
+      try {
+        const first = getCountryCallingCode(normalizedAllowed[0] as any);
+        return normalizedAllowed.every(
+          (c) => getCountryCallingCode(c as any) === first
+        );
+      } catch {
+        return false;
+      }
+    })();

Add import:

import { isSupportedCountry, getCountryCallingCode } from "libphonenumber-js";

129-136: Mirror normalization and dial-code locking on web path

Keep behavior consistent between platform and web.

-  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;
+  const normalizedAllowed =
+    Array.isArray(allowedCountryCodes) && allowedCountryCodes.length > 0
+      ? allowedCountryCodes
+          .map((c) => c?.toUpperCase())
+          .filter((c): c is string => !!c && isSupportedCountry(c as any))
+      : undefined;
+  const onlyCountries = normalizedAllowed?.map((c) => c.toLowerCase());
+  const effectiveDefaultCountry =
+    onlyCountries && onlyCountries.length > 0 ? onlyCountries[0] : defaultCountry;
+  const lockDialCode =
+    !!normalizedAllowed &&
+    normalizedAllowed.length > 0 &&
+    (() => {
+      try {
+        const first = getCountryCallingCode(normalizedAllowed[0] as any);
+        return normalizedAllowed.every(
+          (c) => getCountryCallingCode(c as any) === first
+        );
+      } catch {
+        return false;
+      }
+    })();

Add import if not added already:

import { isSupportedCountry, getCountryCallingCode } from "libphonenumber-js";

194-198: Harden default-country fallback

Validate the navigator-derived region before using it to avoid unsupported values like “001”.

-      if (isSupportedCountry(data?.countryCode)) {
-        setDefaultCountry(data.countryCode.toLowerCase());
-      } else {
-        setDefaultCountry(navigator.language.split("-")[1]?.toLowerCase() || "us");
-      }
+      if (isSupportedCountry((data.countryCode as any))) {
+        setDefaultCountry(data.countryCode.toLowerCase());
+      } else {
+        const navRegion = navigator.language.split("-")[1]?.toUpperCase();
+        setDefaultCountry(
+          navRegion && isSupportedCountry(navRegion as any)
+            ? navRegion.toLowerCase()
+            : "us"
+        );
+      }

39-54: Sanitization effect runs only once; confirm intended behavior

Comment says “trigger validation on prefill value changes” but the effect ignores value changes. If the prop can change after mount, consider adding value to deps.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 056d830 and 8fcf65a.

📒 Files selected for processing (2)
  • packages/features/bookings/lib/getBookingFields.ts (2 hunks)
  • packages/features/components/phone-input/PhoneInput.tsx (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/features/bookings/lib/getBookingFields.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Always use t() for text localization in frontend code; direct text embedding should trigger a warning

Files:

  • packages/features/components/phone-input/PhoneInput.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/features/components/phone-input/PhoneInput.tsx
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • packages/features/components/phone-input/PhoneInput.tsx
🧠 Learnings (2)
📓 Common learnings
Learnt from: supalarry
PR: calcom/cal.com#23364
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/internal-to-api.spec.ts:295-296
Timestamp: 2025-08-27T13:32:46.887Z
Learning: In calcom/cal.com, when transforming booking fields from internal to API format, tests in organizations-event-types.e2e-spec.ts already expect name field label and placeholder to be empty strings ("") rather than undefined. PR changes that set these to explicit empty strings are typically fixing implementation to match existing test expectations rather than breaking changes.
Learnt from: supalarry
PR: calcom/cal.com#23217
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts:93-94
Timestamp: 2025-08-21T13:44:06.805Z
Learning: In apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts, repository functions that use explicit Prisma select clauses (like getEventTypeWithSeats) are used for specific purposes and don't need to include all EventType fields like bookingRequiresAuthentication. These methods don't feed into the general OutputEventTypesService_2024_06_14 flow.
📚 Learning: 2025-08-08T10:26:13.362Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22919
File: packages/features/calAIPhone/providers/retellAI/services/PhoneNumberService.ts:212-220
Timestamp: 2025-08-08T10:26:13.362Z
Learning: In calcom/cal.com PR #22919, packages/features/calAIPhone/providers/retellAI/services/PhoneNumberService.ts should include the phone number in client-facing HttpError messages (e.g., in updatePhoneNumber/getPhoneNumber catch blocks). Do not suggest redacting the phone number from these errors unless requirements change (per maintainer: Udit-takkar).

Applied to files:

  • packages/features/components/phone-input/PhoneInput.tsx
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Production builds / Build API v1
  • GitHub Check: Production builds / Build Web App
  • GitHub Check: Production builds / Build Docs
  • GitHub Check: Tests / Unit
  • GitHub Check: Production builds / Build Atoms
  • GitHub Check: Production builds / Build API v2
  • GitHub Check: Type check / check-types
  • GitHub Check: Linters / lint
🔇 Additional comments (4)
packages/features/components/phone-input/PhoneInput.tsx (4)

58-66: LGTM: Prop plumbing to web variant

Forwarding allowedCountryCodes into BasePhoneInputWeb looks correct.


118-127: LGTM: Web variant signature

allowedCountryCodes threading into BasePhoneInputWeb is correct.


83-86: Replace disableCountryCode with countryCodeEditable and enforce restriction via isValid

disableCountryCode doesn’t reliably lock the prefix. Use countryCodeEditable and add isValid to prevent disallowed countries at input time.

-      country={effectiveDefaultCountry}
-      onlyCountries={onlyCountries && onlyCountries.length > 0 ? onlyCountries : undefined}
-      disableCountryCode={singleCountry}
+      country={effectiveDefaultCountry}
+      onlyCountries={onlyCountries && onlyCountries.length > 0 ? onlyCountries : undefined}
+      countryCodeEditable={!lockDialCode}
+      disableDropdown={!!onlyCountries && onlyCountries.length === 1}
+      isValid={(_, country) =>
+        !onlyCountries ||
+        onlyCountries.includes(country?.countryCode?.toLowerCase() || "")
+      }

141-146: Lock prefix and validate allowed countries in web path

Same as platform path; this also addresses QA’s “I can still add any phone number” concern.

-      country={value ? undefined : effectiveDefaultCountry}
+      country={value ? undefined : effectiveDefaultCountry}
@@
-      onlyCountries={onlyCountries && onlyCountries.length > 0 ? onlyCountries : undefined}
-      disableCountryCode={singleCountry}
+      onlyCountries={onlyCountries && onlyCountries.length > 0 ? onlyCountries : undefined}
+      countryCodeEditable={!lockDialCode}
+      disableDropdown={!!onlyCountries && onlyCountries.length === 1}
+      isValid={(_, country) =>
+        !onlyCountries ||
+        onlyCountries.includes(country?.countryCode?.toLowerCase() || "")
+      }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/features/ee/workflows/components/WorkflowStepContainer.tsx (2)

437-449: Wrong toast severity on failed phone verification

When isVerified is false, you’re still using a “success” toast. Use error for wrong code.

-      showToast(isVerified ? t("verified_successfully") : t("wrong_code"), "success");
+      showToast(isVerified ? t("verified_successfully") : t("wrong_code"), isVerified ? "success" : "error");

475-487: Bug: email verification flag set to true even on wrong code; wrong toast severity

setEmailVerified(true) should respect isVerified. Also use “error” toast when code is wrong.

-    onSuccess: (isVerified) => {
-      showToast(isVerified ? t("verified_successfully") : t("wrong_code"), "success");
-      setEmailVerified(true);
+    onSuccess: (isVerified) => {
+      showToast(isVerified ? t("verified_successfully") : t("wrong_code"), isVerified ? "success" : "error");
+      setEmailVerified(isVerified);
       if (
         step &&
         form?.formState?.errors?.steps &&
         form.formState.errors.steps[step.stepNumber - 1]?.sendTo &&
         isVerified
       ) {
         form.clearErrors(`steps.${step.stepNumber - 1}.sendTo`);
       }
🧹 Nitpick comments (5)
packages/features/ee/workflows/components/WorkflowStepContainer.tsx (5)

97-107: Minor TS cleanup: avoid non-null assertion

You already guard triggerMap[trigger]; the non-null assertion is unnecessary. Slightly clearer:

-  return t(triggerMap[trigger]!);
+  const key = triggerMap[trigger];
+  if (!key) return null;
+  return t(key);

239-244: Normalize and precompute sets for membership checks

Current arrays cause repeated linear scans and inconsistent normalization across call sites. Normalize once and use Sets for O(1) lookups.

-  const verifiedNumbers = useMemo(
-    () => _verifiedNumbers?.map((number) => number.phoneNumber) || [],
-    [_verifiedNumbers]
-  );
-  const verifiedEmails = useMemo(() => _verifiedEmails || [], [_verifiedEmails]);
+  const verifiedNumbers = useMemo(
+    () => (_verifiedNumbers?.map((n) => n.phoneNumber) ?? []),
+    [_verifiedNumbers]
+  );
+  const verifiedEmails = useMemo(() => (_verifiedEmails ?? []), [_verifiedEmails]);
+  const normalizePhone = useCallback((s?: string) => s?.replace(/[^\d+]/g, "") ?? "", []);
+  const verifiedNumberSet = useMemo(
+    () => new Set(verifiedNumbers.map(normalizePhone)),
+    [verifiedNumbers, normalizePhone]
+  );
+  const verifiedEmailSet = useMemo(
+    () => new Set(verifiedEmails.map((e) => e.toLowerCase().trim())),
+    [verifiedEmails]
+  );

401-408: Email verification status: compare case-insensitively

Email addresses are case-insensitive; current strict equality can misreport.

-  const getEmailVerificationStatus = useCallback(
-    () =>
-      !!step &&
-      !!verifiedEmails.find(
-        (email: string) => email === form.getValues(`steps.${step.stepNumber - 1}.sendTo`)
-      ),
-    [step, verifiedEmails, form]
-  );
+  const getEmailVerificationStatus = useCallback(() => {
+    if (!step) return false;
+    const sendTo = (form.getValues(`steps.${step.stepNumber - 1}.sendTo`) as string | undefined)
+      ?.toLowerCase()
+      .trim();
+    return !!sendTo && verifiedEmailSet.has(sendTo);
+  }, [step, verifiedEmailSet, form]);

413-415: Keep verification flags in sync on programmatic value changes

If sendTo changes via form.setValue (e.g., action switch), effects depending only on callbacks won’t re-run. Subscribe to form changes:

useEffect(() => {
  const sub = form.watch((_, { name }) => {
    if (!name?.endsWith(".sendTo")) return;
    setNumberVerified(getNumberVerificationStatus());
    setEmailVerified(getEmailVerificationStatus());
  });
  return () => sub.unsubscribe();
}, [form, getNumberVerificationStatus, getEmailVerificationStatus]);

Verify this is necessary in your flows (e.g., switching actions sets sendTo programmatically).


239-244: Consistency: remove unnecessary array clone in onChange checks

Elsewhere you use .concat([]) before .find(). It’s not needed; .find() is non-mutating. After adopting Sets as suggested, this also becomes moot.

Also applies to: 392-399, 401-408

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8fcf65a and ea7fe5e.

📒 Files selected for processing (1)
  • packages/features/ee/workflows/components/WorkflowStepContainer.tsx (4 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Always use t() for text localization in frontend code; direct text embedding should trigger a warning

Files:

  • packages/features/ee/workflows/components/WorkflowStepContainer.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/features/ee/workflows/components/WorkflowStepContainer.tsx
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • packages/features/ee/workflows/components/WorkflowStepContainer.tsx
🧠 Learnings (5)
📓 Common learnings
Learnt from: supalarry
PR: calcom/cal.com#23364
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/internal-to-api.spec.ts:295-296
Timestamp: 2025-08-27T13:32:46.887Z
Learning: In calcom/cal.com, when transforming booking fields from internal to API format, tests in organizations-event-types.e2e-spec.ts already expect name field label and placeholder to be empty strings ("") rather than undefined. PR changes that set these to explicit empty strings are typically fixing implementation to match existing test expectations rather than breaking changes.
Learnt from: supalarry
PR: calcom/cal.com#23217
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts:93-94
Timestamp: 2025-08-21T13:44:06.805Z
Learning: In apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts, repository functions that use explicit Prisma select clauses (like getEventTypeWithSeats) are used for specific purposes and don't need to include all EventType fields like bookingRequiresAuthentication. These methods don't feed into the general OutputEventTypesService_2024_06_14 flow.
📚 Learning: 2025-08-26T20:09:17.089Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22995
File: packages/features/ee/workflows/components/WorkflowStepContainer.tsx:641-649
Timestamp: 2025-08-26T20:09:17.089Z
Learning: In packages/features/ee/workflows/components/WorkflowStepContainer.tsx, Cal.AI actions are intentionally filtered out/hidden for organization workflows when props.isOrganization is true. This restriction is by design per maintainer Udit-takkar in PR #22995, despite the broader goal of enabling Cal.AI self-serve.

Applied to files:

  • packages/features/ee/workflows/components/WorkflowStepContainer.tsx
📚 Learning: 2025-09-08T09:34:46.661Z
Learnt from: CarinaWolli
PR: calcom/cal.com#23421
File: packages/features/ee/workflows/components/AddActionDialog.tsx:171-174
Timestamp: 2025-09-08T09:34:46.661Z
Learning: In packages/features/ee/workflows/components/AddActionDialog.tsx, the actionOptions prop is guaranteed to never be empty according to maintainer CarinaWolli, so defensive checks for empty arrays are not necessary.

Applied to files:

  • packages/features/ee/workflows/components/WorkflowStepContainer.tsx
📚 Learning: 2025-08-15T00:27:33.280Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22995
File: packages/features/ee/workflows/components/AgentConfigurationSheet.tsx:348-371
Timestamp: 2025-08-15T00:27:33.280Z
Learning: In calcom/cal.com workflows and AI agent components, variable insertion follows a consistent pattern of directly transforming the input variable with toUpperCase() and replace(/ /g, "_") to create tokens like {VARIABLE_NAME}. The AgentConfigurationSheet.tsx implementation correctly follows this same pattern as WorkflowStepContainer.tsx, per maintainer Udit-takkar in PR #22995.

Applied to files:

  • packages/features/ee/workflows/components/WorkflowStepContainer.tsx
📚 Learning: 2025-08-15T00:27:33.280Z
Learnt from: Udit-takkar
PR: calcom/cal.com#22995
File: packages/features/ee/workflows/components/AgentConfigurationSheet.tsx:348-371
Timestamp: 2025-08-15T00:27:33.280Z
Learning: In calcom/cal.com, variable insertion in both WorkflowStepContainer.tsx (addVariableEmailSubject) and AgentConfigurationSheet.tsx (addVariableToGeneralPrompt) follows the identical pattern: `{${variable.toUpperCase().replace(/ /g, "_")}}`. This is the established, consistent approach across the codebase for transforming variable labels into tokens, per maintainer Udit-takkar in PR #22995.

Applied to files:

  • packages/features/ee/workflows/components/WorkflowStepContainer.tsx
🔇 Additional comments (1)
packages/features/ee/workflows/components/WorkflowStepContainer.tsx (1)

4-4: LGTM: added React hooks import

The added useCallback/useEffect/useMemo/useRef/useState imports align with later memoization/callback usage.

Comment on lines +392 to +399
const getNumberVerificationStatus = useCallback(
() =>
!!step &&
!!verifiedNumbers.find(
(number: string) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`)
),
[step, verifiedNumbers, form]
);
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 | 🟠 Major

Phone verification status: normalize and use Set

Direct string equality will false-negative due to formatting (spaces, dashes). Also prefer Set lookup for performance.

-  const getNumberVerificationStatus = useCallback(
-    () =>
-      !!step &&
-      !!verifiedNumbers.find(
-        (number: string) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`)
-      ),
-    [step, verifiedNumbers, form]
-  );
+  const getNumberVerificationStatus = useCallback(() => {
+    if (!step) return false;
+    const sendTo = form.getValues(`steps.${step.stepNumber - 1}.sendTo`) as string | undefined;
+    return verifiedNumberSet.has(normalizePhone(sendTo));
+  }, [step, verifiedNumberSet, normalizePhone, form]);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/features/ee/workflows/components/WorkflowStepContainer.tsx around
lines 392-399, the current phone verification check uses raw string equality
which can false-negative due to formatting; replace it by normalizing numbers
and using a Set lookup: create a memoized Set (useMemo) of verifiedNumbers
normalized to digits only (remove non-digit chars) and useCallback to normalize
the form value for steps.{step.stepNumber - 1}.sendTo to digits-only, then
return !!step && normalizedSet.has(normalizedValue); update the hook
dependencies to include step and the memoized Set (and form if needed) so
lookups are correct and performant.

return plainToClass(
EventTypeOutput_2024_06_14,
this.outputEventTypesService.getResponseEventType(eventType.ownerId, eventType, false),
this.outputEventTypesService.getResponseEventType(eventType.ownerId, eventType),
Copy link
Contributor Author

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

/>
)}

{formFieldType === "phone" && (
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Move selecting allowed countries for phone inputs to the booking field level

Copy link
Member

@CarinaWolli CarinaWolli left a comment

Choose a reason for hiding this comment

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

It still let's me submit a different country code. See here:

--> Allowed Country codes --> US & Germany:

Screenshot 2025-09-30 at 10 42 06 AM

--> Shows correct in the dropdown, but I can still type another country code:

Screenshot 2025-09-30 at 10 48 18 AM

--> No error message, booking is successful with a country code that is not supported:

Screenshot 2025-09-30 at 10 44 47 AM

}: {
workflowId: Workflow["id"];
isSmsReminderNumberRequired: boolean;
allowedCountryCodes?: string[];
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
allowedCountryCodes?: string[];
allowedCountryCodes?: string[];

This is not used

newResponses[field.name] = parsedValue;
} else if (field.type === "phone") {
newResponses[field.name] = ensureValidPhoneNumber(value);
// Auto-prepend country code for single country restrictions
Copy link
Member

Choose a reason for hiding this comment

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

I think the single country restriction causes issues for many countries. For example, in Austria if I don’t see +43, I naturally enter my number with a leading 0:
Screenshot 2025-09-30 at 10 57 07 AM

But then we change it to: +430680123456
Screenshot 2025-09-30 at 11 00 01 AM

Which is the wrong international format, as it should be +43 680123456

@github-actions github-actions bot marked this pull request as draft September 30, 2025 09:02
@joeauyeung
Copy link
Contributor Author

Deprio for now

@joeauyeung joeauyeung closed this Dec 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core area: core, team members only enterprise area: enterprise, audit log, organisation, SAML, SSO ❗️ .env changes contains changes to env variables ✨ feature New feature or request ❗️ migrations contains migration files ready-for-e2e size/L workflows area: workflows, automations

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants