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 (