fix(companion): iOS event type detail fixes (#26845)

* fix(companion): iOS event type detail fixes

- Fix #1: Weekly schedule now shows multiple time slots per day in AvailabilityTab
- Fix #2: Allow numeric input fields to be empty with proper validation
- Fix #3: Add native iOS date picker for date range in Limits tab
- Fix #4: Change 'Email verification' to 'Booker email verification' in Advanced tab
- Fix #5: Fix Requires confirmation comparison logic in buildPartialUpdatePayload
- Fix #6: Forward parameters toggle already working, verified wiring
- Fix #7: Add toggles for redirect booking URL and interface language in Advanced tab
- Fix #8: Increase scroll padding at bottom from 200 to 350 for better keyboard access

* fix(companion): conditional scroll padding for limits and advanced tabs only

* fix(companion): remove static date text from iOS date picker, keep only picker

* fix(companion): simplify iOS date picker to match RescheduleScreen pattern

* update code
This commit is contained in:
Dhairyashil Shinde
2026-01-14 19:58:44 +01:00
committed by GitHub
parent 5778409055
commit ff65dee89f
8 changed files with 245 additions and 115 deletions
@@ -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;
// 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
);
});
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}
+16 -14
View File
@@ -61,20 +61,22 @@ export function LoginScreen() {
<Text className="text-[17px] font-semibold text-white">Continue with Cal.com</Text>
</TouchableOpacity>
{/* Sign up link */}
<TouchableOpacity
onPress={handleSignUp}
className="mt-3 items-center justify-center py-1"
style={Platform.OS === "web" ? { cursor: "pointer" } : undefined}
activeOpacity={0.7}
>
<View>
<Text className="text-[15px] text-gray-500">
Don't have an account? <Text className="font-semibold text-gray-900">Sign up</Text>
</Text>
<View className="h-px bg-gray-400" style={{ marginTop: 2 }} />
</View>
</TouchableOpacity>
{/* Sign up link - hidden on iOS */}
{Platform.OS !== "ios" && (
<TouchableOpacity
onPress={handleSignUp}
className="mt-3 items-center justify-center py-1"
style={Platform.OS === "web" ? { cursor: "pointer" } : undefined}
activeOpacity={0.7}
>
<View>
<Text className="text-[15px] text-gray-500">
Don't have an account? <Text className="font-semibold text-gray-900">Sign up</Text>
</Text>
<View className="h-px bg-gray-400" style={{ marginTop: 2 }} />
</View>
</TouchableOpacity>
)}
</View>
</View>
);
@@ -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);
}}
/>
<SettingRow
title="Email verification"
title="Booker email verification"
description="To ensure booker's email verification before scheduling events."
value={props.requiresBookerEmailVerification}
onValueChange={props.setRequiresBookerEmailVerification}
@@ -385,7 +398,16 @@ export function AdvancedTab(props: AdvancedTabProps) {
title="Offer seats"
description="Offer seats for booking. This automatically disables guest & opt-in bookings."
value={props.seatsEnabled}
onValueChange={props.setSeatsEnabled}
onValueChange={(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 */}
<SettingsGroup header="Language">
<NavigationRow
<SettingRow
isFirst
isLast
title="Interface Language"
value={getLanguageLabel(props.interfaceLanguage)}
onPress={() => 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 ? (
<NavigationRow
isLast
title="Select Language"
value={getLanguageLabel(props.interfaceLanguage)}
onPress={() => setShowLanguagePicker(true)}
options={interfaceLanguageOptions}
onSelect={props.setInterfaceLanguage}
/>
) : null}
</SettingsGroup>
{/* Language Picker Modal */}
@@ -527,34 +558,42 @@ export function AdvancedTab(props: AdvancedTabProps) {
{/* Redirect */}
<SettingsGroup header="Redirect">
<View className="bg-white pl-4">
<View className="border-b border-[#E5E5E5] pt-4 pb-3 pr-4">
<Text className="mb-2 text-[13px] text-[#6D6D72]">
Redirect URL after successful booking
</Text>
<TextInput
className="rounded-lg bg-[#F2F2F7] px-3 py-2 text-[17px] text-black"
value={props.successRedirectUrl}
onChangeText={props.setSuccessRedirectUrl}
placeholder="https://example.com/thank-you"
placeholderTextColor="#8E8E93"
keyboardType="url"
autoCapitalize="none"
/>
{props.successRedirectUrl ? (
<Text className="mt-2 text-[13px] text-[#FF9500]">
Adding a redirect will disable the success page.
</Text>
) : null}
</View>
</View>
<SettingRow
title="Forward parameters"
description="Forward parameters such as ?email=...&name=... to the redirect URL."
value={props.forwardParamsSuccessRedirect}
onValueChange={props.setForwardParamsSuccessRedirect}
isLast
isFirst
title="Redirect on booking"
description="Redirect to a custom URL after a successful booking."
value={props.redirectEnabled}
onValueChange={props.setRedirectEnabled}
isLast={!props.redirectEnabled}
/>
{props.redirectEnabled ? (
<>
<View className="bg-white pl-4">
<View className="border-b border-[#E5E5E5] pt-4 pb-3 pr-4">
<Text className="mb-2 text-[13px] text-[#6D6D72]">Redirect URL</Text>
<TextInput
className="rounded-lg bg-[#F2F2F7] px-3 py-2 text-[17px] text-black"
value={props.successRedirectUrl}
onChangeText={props.setSuccessRedirectUrl}
placeholder="https://example.com/thank-you"
placeholderTextColor="#8E8E93"
keyboardType="url"
autoCapitalize="none"
/>
<Text className="mt-2 text-[13px] text-[#FF9500]">
Adding a redirect will disable the success page.
</Text>
</View>
</View>
<SettingRow
title="Forward parameters"
description="Forward parameters such as ?email=...&name=... to the redirect URL."
value={props.forwardParamsSuccessRedirect}
onValueChange={props.setForwardParamsSuccessRedirect}
isLast
/>
</>
) : null}
</SettingsGroup>
{/* Configure on Web Section */}
@@ -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 (
<View
@@ -203,16 +210,25 @@ export function AvailabilityTab(props: AvailabilityTabProps) {
{day}
</Text>
{/* Time range or Unavailable */}
<Text
className={`flex-1 text-right text-[15px] ${
isEnabled ? "text-black" : "text-[#8E8E93]"
}`}
>
{isEnabled && dayInfo?.startTime && dayInfo?.endTime
? `${formatTime12Hour(dayInfo.startTime)} - ${formatTime12Hour(dayInfo.endTime)}`
: "Unavailable"}
</Text>
{/* Time ranges or Unavailable - support multiple time slots */}
{isEnabled && timeSlots.length > 0 ? (
<View className="flex-1 items-end">
{timeSlots.map((slot, slotIndex) => (
<Text
key={`${slotIndex}-${slot.startTime}`}
className={`text-[15px] text-black ${slotIndex > 0 ? "mt-1" : ""}`}
>
{slot.startTime && slot.endTime
? `${formatTime12Hour(slot.startTime)} - ${formatTime12Hour(slot.endTime)}`
: ""}
</Text>
))}
</View>
) : (
<Text className="flex-1 text-right text-[15px] text-[#8E8E93]">
Unavailable
</Text>
)}
</View>
);
})}
@@ -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) {
<View className="flex-1">
<Text className="mb-2 text-[17px] text-black">Within a date range</Text>
{props.futureBookingType === "range" ? (
<View className="gap-2">
<TextInput
className="rounded-lg bg-[#F2F2F7] px-3 py-2 text-[15px] text-black"
value={props.rangeStartDate}
onChangeText={props.setRangeStartDate}
placeholder="Start date (YYYY-MM-DD)"
placeholderTextColor="#8E8E93"
/>
<TextInput
className="rounded-lg bg-[#F2F2F7] px-3 py-2 text-[15px] text-black"
value={props.rangeEndDate}
onChangeText={props.setRangeEndDate}
placeholder="End date (YYYY-MM-DD)"
placeholderTextColor="#8E8E93"
/>
<View className="gap-3">
<View className="flex-row items-center">
<Text className="w-20 text-[13px] text-[#6D6D72]">Start date</Text>
<LimitsTabDatePicker
value={props.rangeStartDate}
onChange={props.setRangeStartDate}
placeholder="Select start date"
/>
</View>
<View className="flex-row items-center">
<Text className="w-20 text-[13px] text-[#6D6D72]">End date</Text>
<LimitsTabDatePicker
value={props.rangeEndDate}
onChange={props.setRangeEndDate}
placeholder="Select end date"
/>
</View>
</View>
) : null}
</View>
@@ -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 (
<Host matchContents>
<DatePicker
onDateChange={handleDateChange}
displayedComponents={["date"]}
selection={dateValue}
/>
</Host>
);
}
@@ -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 (
<View>
<TextInput
className="rounded-lg bg-[#F2F2F7] px-3 py-2 text-[15px] text-black"
value={value}
onChangeText={onChange}
placeholder={placeholder || "YYYY-MM-DD"}
placeholderTextColor="#8E8E93"
/>
</View>
);
}
@@ -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 (