diff --git a/companion/app/(tabs)/(event-types)/event-type-detail.tsx b/companion/app/(tabs)/(event-types)/event-type-detail.tsx
index cbef9d914941ac..78552bdfa210e6 100644
--- a/companion/app/(tabs)/(event-types)/event-type-detail.tsx
+++ b/companion/app/(tabs)/(event-types)/event-type-detail.tsx
@@ -220,6 +220,7 @@ export default function EventTypeDetail() {
const [requiresBookerEmailVerification, setRequiresBookerEmailVerification] = useState(false);
const [hideCalendarNotes, setHideCalendarNotes] = useState(false);
const [hideCalendarEventDetails, setHideCalendarEventDetails] = useState(false);
+ const [redirectEnabled, setRedirectEnabled] = useState(false);
const [successRedirectUrl, setSuccessRedirectUrl] = useState("");
const [forwardParamsSuccessRedirect, setForwardParamsSuccessRedirect] = useState(false);
const [hideOrganizerEmail, setHideOrganizerEmail] = useState(false);
@@ -231,6 +232,7 @@ export default function EventTypeDetail() {
const [customReplyToEmail, setCustomReplyToEmail] = useState("");
const [eventTypeColorLight, setEventTypeColorLight] = useState("#292929");
const [eventTypeColorDark, setEventTypeColorDark] = useState("#FAFAFA");
+ const [interfaceLanguageEnabled, setInterfaceLanguageEnabled] = useState(false);
const [interfaceLanguage, setInterfaceLanguage] = useState("");
const [showOptimizedSlots, setShowOptimizedSlots] = useState(false);
@@ -673,8 +675,8 @@ export default function EventTypeDetail() {
setSendCalVideoTranscription(false);
}
- // Load interface language (API V2)
- if (eventTypeExt.interfaceLanguage !== undefined) {
+ if (eventTypeExt.interfaceLanguage) {
+ setInterfaceLanguageEnabled(true);
setInterfaceLanguage(eventTypeExt.interfaceLanguage);
}
@@ -737,8 +739,8 @@ export default function EventTypeDetail() {
setHideOrganizerEmail(eventTypeExt.hideOrganizerEmail);
}
- // Load redirect URL
if (eventType.successRedirectUrl) {
+ setRedirectEnabled(true);
setSuccessRedirectUrl(eventType.successRedirectUrl);
}
if (eventType.forwardParamsSuccessRedirect !== undefined) {
@@ -921,25 +923,34 @@ export default function EventTypeDetail() {
const dayShort = day.substring(0, 3).toLowerCase(); // mon, tue, etc.
const dayShortUpper = day.substring(0, 3).toUpperCase();
- const availability = selectedScheduleDetails.availability?.find((avail) => {
- if (!avail.days || !Array.isArray(avail.days)) return false;
-
- return avail.days.some(
- (d) =>
- d === dayLower ||
- d === dayUpper ||
- d === day ||
- d === dayShort ||
- d === dayShortUpper ||
- d.toLowerCase() === dayLower
- );
- });
+ // Find ALL matching availability slots for this day (not just the first one)
+ const matchingSlots =
+ selectedScheduleDetails.availability?.filter((avail) => {
+ if (!avail.days || !Array.isArray(avail.days)) return false;
+
+ return avail.days.some(
+ (d) =>
+ d === dayLower ||
+ d === dayUpper ||
+ d === day ||
+ d === dayShort ||
+ d === dayShortUpper ||
+ d.toLowerCase() === dayLower
+ );
+ }) || [];
+
+ // Map to time slots array
+ const timeSlots = matchingSlots.map((slot) => ({
+ startTime: slot.startTime,
+ endTime: slot.endTime,
+ }));
return {
day,
- available: !!availability,
- startTime: availability?.startTime,
- endTime: availability?.endTime,
+ available: timeSlots.length > 0,
+ startTime: timeSlots[0]?.startTime,
+ endTime: timeSlots[0]?.endTime,
+ timeSlots, // Include all time slots for this day
};
});
@@ -1400,7 +1411,10 @@ export default function EventTypeDetail() {
style={{
flex: 1,
}}
- contentContainerStyle={{ padding: 16, paddingBottom: 200 }}
+ contentContainerStyle={{
+ padding: 16,
+ paddingBottom: activeTab === "limits" || activeTab === "advanced" ? 280 : 200,
+ }}
contentInsetAdjustmentBehavior="automatic"
>
{activeTab === "basics" ? (
@@ -2161,6 +2175,8 @@ export default function EventTypeDetail() {
setAllowReschedulingPastEvents={setAllowReschedulingPastEvents}
allowBookingThroughRescheduleLink={allowBookingThroughRescheduleLink}
setAllowBookingThroughRescheduleLink={setAllowBookingThroughRescheduleLink}
+ redirectEnabled={redirectEnabled}
+ setRedirectEnabled={setRedirectEnabled}
successRedirectUrl={successRedirectUrl}
setSuccessRedirectUrl={setSuccessRedirectUrl}
forwardParamsSuccessRedirect={forwardParamsSuccessRedirect}
@@ -2189,6 +2205,8 @@ export default function EventTypeDetail() {
setDisableRescheduling={setDisableRescheduling}
sendCalVideoTranscription={sendCalVideoTranscription}
setSendCalVideoTranscription={setSendCalVideoTranscription}
+ interfaceLanguageEnabled={interfaceLanguageEnabled}
+ setInterfaceLanguageEnabled={setInterfaceLanguageEnabled}
interfaceLanguage={interfaceLanguage}
setInterfaceLanguage={setInterfaceLanguage}
showOptimizedSlots={showOptimizedSlots}
diff --git a/companion/components/LoginScreen.tsx b/companion/components/LoginScreen.tsx
index 2e896536ca7d9d..3febdebae46da8 100644
--- a/companion/components/LoginScreen.tsx
+++ b/companion/components/LoginScreen.tsx
@@ -61,20 +61,22 @@ export function LoginScreen() {
Continue with Cal.com
- {/* Sign up link */}
-
-
-
- Don't have an account? Sign up
-
-
-
-
+ {/* Sign up link - hidden on iOS */}
+ {Platform.OS !== "ios" && (
+
+
+
+ Don't have an account? Sign up
+
+
+
+
+ )}
);
diff --git a/companion/components/event-type-detail/tabs/AdvancedTab.tsx b/companion/components/event-type-detail/tabs/AdvancedTab.tsx
index 0a009da1ccba25..3e55b519a86e8a 100644
--- a/companion/components/event-type-detail/tabs/AdvancedTab.tsx
+++ b/companion/components/event-type-detail/tabs/AdvancedTab.tsx
@@ -246,6 +246,8 @@ interface AdvancedTabProps {
setAllowReschedulingPastEvents: (value: boolean) => void;
allowBookingThroughRescheduleLink: boolean;
setAllowBookingThroughRescheduleLink: (value: boolean) => void;
+ redirectEnabled: boolean;
+ setRedirectEnabled: (value: boolean) => void;
successRedirectUrl: string;
setSuccessRedirectUrl: (value: string) => void;
forwardParamsSuccessRedirect: boolean;
@@ -271,6 +273,8 @@ interface AdvancedTabProps {
setDisableRescheduling: (value: boolean) => void;
sendCalVideoTranscription: boolean;
setSendCalVideoTranscription: (value: boolean) => void;
+ interfaceLanguageEnabled: boolean;
+ setInterfaceLanguageEnabled: (value: boolean) => void;
interfaceLanguage: string;
setInterfaceLanguage: (value: string) => void;
showOptimizedSlots: boolean;
@@ -294,10 +298,19 @@ export function AdvancedTab(props: AdvancedTabProps) {
title="Requires confirmation"
description="The booking needs to be manually confirmed before it is pushed to your calendar and a confirmation is sent."
value={props.requiresConfirmation}
- onValueChange={props.setRequiresConfirmation}
+ onValueChange={(value) => {
+ if (value && props.seatsEnabled) {
+ Alert.alert(
+ "Disable 'Offer seats' first",
+ "You need to:\n1. Disable 'Offer seats' and Save\n2. Then enable 'Requires confirmation' and Save again"
+ );
+ return;
+ }
+ props.setRequiresConfirmation(value);
+ }}
/>
{
+ if (value && props.requiresConfirmation) {
+ Alert.alert(
+ "Disable 'Requires confirmation' first",
+ "You need to:\n1. Disable 'Requires confirmation' and Save\n2. Then enable 'Offer seats' and Save again"
+ );
+ return;
+ }
+ props.setSeatsEnabled(value);
+ }}
learnMoreUrl="https://cal.com/help/event-types/offer-seats"
isLast
/>
@@ -438,15 +460,24 @@ export function AdvancedTab(props: AdvancedTabProps) {
{/* Language */}
- setShowLanguagePicker(true)}
- options={interfaceLanguageOptions}
- onSelect={props.setInterfaceLanguage}
+ title="Custom interface language"
+ description="Override the default browser language for the booking page."
+ value={props.interfaceLanguageEnabled}
+ onValueChange={props.setInterfaceLanguageEnabled}
+ isLast={!props.interfaceLanguageEnabled}
/>
+ {props.interfaceLanguageEnabled ? (
+ setShowLanguagePicker(true)}
+ options={interfaceLanguageOptions}
+ onSelect={props.setInterfaceLanguage}
+ />
+ ) : null}
{/* Language Picker Modal */}
@@ -527,34 +558,42 @@ export function AdvancedTab(props: AdvancedTabProps) {
{/* Redirect */}
-
-
-
- Redirect URL after successful booking
-
-
- {props.successRedirectUrl ? (
-
- Adding a redirect will disable the success page.
-
- ) : null}
-
-
+ {props.redirectEnabled ? (
+ <>
+
+
+ Redirect URL
+
+
+ Adding a redirect will disable the success page.
+
+
+
+
+ >
+ ) : null}
{/* Configure on Web Section */}
diff --git a/companion/components/event-type-detail/tabs/AvailabilityTab.tsx b/companion/components/event-type-detail/tabs/AvailabilityTab.tsx
index 76c4381ff23269..8d828b05395806 100644
--- a/companion/components/event-type-detail/tabs/AvailabilityTab.tsx
+++ b/companion/components/event-type-detail/tabs/AvailabilityTab.tsx
@@ -10,11 +10,17 @@ import { Platform, Text, TouchableOpacity, View } from "react-native";
import type { Schedule } from "@/services/calcom";
import { AvailabilityTabIOSPicker } from "./AvailabilityTabIOSPicker";
+interface TimeSlot {
+ startTime?: string;
+ endTime?: string;
+}
+
interface DaySchedule {
day: string;
available: boolean;
startTime?: string;
endTime?: string;
+ timeSlots?: TimeSlot[];
}
interface AvailabilityTabProps {
@@ -181,6 +187,7 @@ export function AvailabilityTab(props: AvailabilityTabProps) {
const dayInfo = daySchedules.find((d) => d.day === day);
const isEnabled = dayInfo?.available ?? false;
const isLast = index === DAYS.length - 1;
+ const timeSlots = dayInfo?.timeSlots || [];
return (
- {/* Time range or Unavailable */}
-
- {isEnabled && dayInfo?.startTime && dayInfo?.endTime
- ? `${formatTime12Hour(dayInfo.startTime)} - ${formatTime12Hour(dayInfo.endTime)}`
- : "Unavailable"}
-
+ {/* Time ranges or Unavailable - support multiple time slots */}
+ {isEnabled && timeSlots.length > 0 ? (
+
+ {timeSlots.map((slot, slotIndex) => (
+ 0 ? "mt-1" : ""}`}
+ >
+ {slot.startTime && slot.endTime
+ ? `${formatTime12Hour(slot.startTime)} - ${formatTime12Hour(slot.endTime)}`
+ : ""}
+
+ ))}
+
+ ) : (
+
+ Unavailable
+
+ )}
);
})}
diff --git a/companion/components/event-type-detail/tabs/LimitsTab.tsx b/companion/components/event-type-detail/tabs/LimitsTab.tsx
index f9609482a945ca..c3f718b9c3b92a 100644
--- a/companion/components/event-type-detail/tabs/LimitsTab.tsx
+++ b/companion/components/event-type-detail/tabs/LimitsTab.tsx
@@ -17,6 +17,7 @@ import {
View,
} from "react-native";
+import { LimitsTabDatePicker } from "./LimitsTabDatePicker";
import { LimitsTabIOSPicker } from "./LimitsTabIOSPicker";
interface FrequencyLimit {
@@ -301,10 +302,7 @@ export function LimitsTab(props: LimitsTabProps) {
value={props.minimumNoticeValue}
onChangeText={(text) => {
const numericValue = text.replace(/[^0-9]/g, "");
- const num = parseInt(numericValue, 10) || 0;
- if (num >= 0) {
- props.setMinimumNoticeValue(numericValue || "0");
- }
+ props.setMinimumNoticeValue(numericValue);
}}
placeholder="1"
placeholderTextColor="#8E8E93"
@@ -389,10 +387,7 @@ export function LimitsTab(props: LimitsTabProps) {
value={limit.value}
onChangeText={(text) => {
const numericValue = text.replace(/[^0-9]/g, "");
- const num = parseInt(numericValue, 10) || 0;
- if (num >= 0) {
- props.updateFrequencyLimit(limit.id, "value", numericValue || "0");
- }
+ props.updateFrequencyLimit(limit.id, "value", numericValue);
}}
placeholder="1"
placeholderTextColor="#8E8E93"
@@ -451,10 +446,7 @@ export function LimitsTab(props: LimitsTabProps) {
value={limit.value}
onChangeText={(text) => {
const numericValue = text.replace(/[^0-9]/g, "");
- const num = parseInt(numericValue, 10) || 0;
- if (num >= 0) {
- props.updateDurationLimit(limit.id, "value", numericValue || "0");
- }
+ props.updateDurationLimit(limit.id, "value", numericValue);
}}
placeholder="60"
placeholderTextColor="#8E8E93"
@@ -512,10 +504,7 @@ export function LimitsTab(props: LimitsTabProps) {
value={props.maxActiveBookingsValue}
onChangeText={(text) => {
const numericValue = text.replace(/[^0-9]/g, "");
- const num = parseInt(numericValue, 10) || 0;
- if (num >= 0) {
- props.setMaxActiveBookingsValue(numericValue || "1");
- }
+ props.setMaxActiveBookingsValue(numericValue);
}}
placeholder="1"
placeholderTextColor="#8E8E93"
@@ -560,7 +549,7 @@ export function LimitsTab(props: LimitsTabProps) {
value={props.rollingDays}
onChangeText={(text) => {
const numericValue = text.replace(/[^0-9]/g, "");
- props.setRollingDays(numericValue || "30");
+ props.setRollingDays(numericValue);
props.setFutureBookingType("rolling");
}}
placeholder="30"
@@ -602,21 +591,23 @@ export function LimitsTab(props: LimitsTabProps) {
Within a date range
{props.futureBookingType === "range" ? (
-
-
-
+
+
+ Start date
+
+
+
+ End date
+
+
) : null}
diff --git a/companion/components/event-type-detail/tabs/LimitsTabDatePicker.ios.tsx b/companion/components/event-type-detail/tabs/LimitsTabDatePicker.ios.tsx
new file mode 100644
index 00000000000000..5b9d1346209c56
--- /dev/null
+++ b/companion/components/event-type-detail/tabs/LimitsTabDatePicker.ios.tsx
@@ -0,0 +1,36 @@
+import { DatePicker, Host } from "@expo/ui/swift-ui";
+import { useCallback, useMemo } from "react";
+
+interface LimitsTabDatePickerProps {
+ value: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+}
+
+export function LimitsTabDatePicker({ value, onChange }: LimitsTabDatePickerProps) {
+ const dateValue = useMemo(() => {
+ if (!value) return new Date();
+ const parsed = new Date(`${value}T00:00:00`);
+ return Number.isNaN(parsed.getTime()) ? new Date() : parsed;
+ }, [value]);
+
+ const handleDateChange = useCallback(
+ (date: Date) => {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ onChange(`${year}-${month}-${day}`);
+ },
+ [onChange]
+ );
+
+ return (
+
+
+
+ );
+}
diff --git a/companion/components/event-type-detail/tabs/LimitsTabDatePicker.tsx b/companion/components/event-type-detail/tabs/LimitsTabDatePicker.tsx
new file mode 100644
index 00000000000000..d798b7b75a8d2b
--- /dev/null
+++ b/companion/components/event-type-detail/tabs/LimitsTabDatePicker.tsx
@@ -0,0 +1,21 @@
+import { TextInput, View } from "react-native";
+
+interface LimitsTabDatePickerProps {
+ value: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+}
+
+export function LimitsTabDatePicker({ value, onChange, placeholder }: LimitsTabDatePickerProps) {
+ return (
+
+
+
+ );
+}
diff --git a/companion/components/event-type-detail/utils/buildPartialUpdatePayload.ts b/companion/components/event-type-detail/utils/buildPartialUpdatePayload.ts
index 3ab73a64f10c43..6b51101217bdd9 100644
--- a/companion/components/event-type-detail/utils/buildPartialUpdatePayload.ts
+++ b/companion/components/event-type-detail/utils/buildPartialUpdatePayload.ts
@@ -643,11 +643,18 @@ export function buildPartialUpdatePayload(
}
const originalRequiresConfirmation =
- original.requiresConfirmation ||
+ original.requiresConfirmation === true ||
(original.confirmationPolicy &&
- !("disabled" in original.confirmationPolicy && original.confirmationPolicy.disabled));
- if (currentState.requiresConfirmation !== originalRequiresConfirmation) {
- payload.requiresConfirmation = currentState.requiresConfirmation;
+ !(
+ "disabled" in original.confirmationPolicy && original.confirmationPolicy.disabled === true
+ ));
+ if (currentState.requiresConfirmation !== !!originalRequiresConfirmation) {
+ // API V2 expects confirmationPolicy object, not requiresConfirmation boolean
+ if (currentState.requiresConfirmation) {
+ payload.confirmationPolicy = { type: "always" };
+ } else {
+ payload.confirmationPolicy = { disabled: true };
+ }
}
if (