Skip to content

Commit ff65dee

Browse files
authored
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
1 parent 5778409 commit ff65dee

8 files changed

Lines changed: 246 additions & 116 deletions

File tree

companion/app/(tabs)/(event-types)/event-type-detail.tsx

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ export default function EventTypeDetail() {
220220
const [requiresBookerEmailVerification, setRequiresBookerEmailVerification] = useState(false);
221221
const [hideCalendarNotes, setHideCalendarNotes] = useState(false);
222222
const [hideCalendarEventDetails, setHideCalendarEventDetails] = useState(false);
223+
const [redirectEnabled, setRedirectEnabled] = useState(false);
223224
const [successRedirectUrl, setSuccessRedirectUrl] = useState("");
224225
const [forwardParamsSuccessRedirect, setForwardParamsSuccessRedirect] = useState(false);
225226
const [hideOrganizerEmail, setHideOrganizerEmail] = useState(false);
@@ -231,6 +232,7 @@ export default function EventTypeDetail() {
231232
const [customReplyToEmail, setCustomReplyToEmail] = useState("");
232233
const [eventTypeColorLight, setEventTypeColorLight] = useState("#292929");
233234
const [eventTypeColorDark, setEventTypeColorDark] = useState("#FAFAFA");
235+
const [interfaceLanguageEnabled, setInterfaceLanguageEnabled] = useState(false);
234236
const [interfaceLanguage, setInterfaceLanguage] = useState("");
235237
const [showOptimizedSlots, setShowOptimizedSlots] = useState(false);
236238

@@ -673,8 +675,8 @@ export default function EventTypeDetail() {
673675
setSendCalVideoTranscription(false);
674676
}
675677

676-
// Load interface language (API V2)
677-
if (eventTypeExt.interfaceLanguage !== undefined) {
678+
if (eventTypeExt.interfaceLanguage) {
679+
setInterfaceLanguageEnabled(true);
678680
setInterfaceLanguage(eventTypeExt.interfaceLanguage);
679681
}
680682

@@ -737,8 +739,8 @@ export default function EventTypeDetail() {
737739
setHideOrganizerEmail(eventTypeExt.hideOrganizerEmail);
738740
}
739741

740-
// Load redirect URL
741742
if (eventType.successRedirectUrl) {
743+
setRedirectEnabled(true);
742744
setSuccessRedirectUrl(eventType.successRedirectUrl);
743745
}
744746
if (eventType.forwardParamsSuccessRedirect !== undefined) {
@@ -921,25 +923,34 @@ export default function EventTypeDetail() {
921923
const dayShort = day.substring(0, 3).toLowerCase(); // mon, tue, etc.
922924
const dayShortUpper = day.substring(0, 3).toUpperCase();
923925

924-
const availability = selectedScheduleDetails.availability?.find((avail) => {
925-
if (!avail.days || !Array.isArray(avail.days)) return false;
926-
927-
return avail.days.some(
928-
(d) =>
929-
d === dayLower ||
930-
d === dayUpper ||
931-
d === day ||
932-
d === dayShort ||
933-
d === dayShortUpper ||
934-
d.toLowerCase() === dayLower
935-
);
936-
});
926+
// Find ALL matching availability slots for this day (not just the first one)
927+
const matchingSlots =
928+
selectedScheduleDetails.availability?.filter((avail) => {
929+
if (!avail.days || !Array.isArray(avail.days)) return false;
930+
931+
return avail.days.some(
932+
(d) =>
933+
d === dayLower ||
934+
d === dayUpper ||
935+
d === day ||
936+
d === dayShort ||
937+
d === dayShortUpper ||
938+
d.toLowerCase() === dayLower
939+
);
940+
}) || [];
941+
942+
// Map to time slots array
943+
const timeSlots = matchingSlots.map((slot) => ({
944+
startTime: slot.startTime,
945+
endTime: slot.endTime,
946+
}));
937947

938948
return {
939949
day,
940-
available: !!availability,
941-
startTime: availability?.startTime,
942-
endTime: availability?.endTime,
950+
available: timeSlots.length > 0,
951+
startTime: timeSlots[0]?.startTime,
952+
endTime: timeSlots[0]?.endTime,
953+
timeSlots, // Include all time slots for this day
943954
};
944955
});
945956

@@ -1400,7 +1411,10 @@ export default function EventTypeDetail() {
14001411
style={{
14011412
flex: 1,
14021413
}}
1403-
contentContainerStyle={{ padding: 16, paddingBottom: 200 }}
1414+
contentContainerStyle={{
1415+
padding: 16,
1416+
paddingBottom: activeTab === "limits" || activeTab === "advanced" ? 280 : 200,
1417+
}}
14041418
contentInsetAdjustmentBehavior="automatic"
14051419
>
14061420
{activeTab === "basics" ? (
@@ -2161,6 +2175,8 @@ export default function EventTypeDetail() {
21612175
setAllowReschedulingPastEvents={setAllowReschedulingPastEvents}
21622176
allowBookingThroughRescheduleLink={allowBookingThroughRescheduleLink}
21632177
setAllowBookingThroughRescheduleLink={setAllowBookingThroughRescheduleLink}
2178+
redirectEnabled={redirectEnabled}
2179+
setRedirectEnabled={setRedirectEnabled}
21642180
successRedirectUrl={successRedirectUrl}
21652181
setSuccessRedirectUrl={setSuccessRedirectUrl}
21662182
forwardParamsSuccessRedirect={forwardParamsSuccessRedirect}
@@ -2189,6 +2205,8 @@ export default function EventTypeDetail() {
21892205
setDisableRescheduling={setDisableRescheduling}
21902206
sendCalVideoTranscription={sendCalVideoTranscription}
21912207
setSendCalVideoTranscription={setSendCalVideoTranscription}
2208+
interfaceLanguageEnabled={interfaceLanguageEnabled}
2209+
setInterfaceLanguageEnabled={setInterfaceLanguageEnabled}
21922210
interfaceLanguage={interfaceLanguage}
21932211
setInterfaceLanguage={setInterfaceLanguage}
21942212
showOptimizedSlots={showOptimizedSlots}

companion/components/LoginScreen.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,20 +61,22 @@ export function LoginScreen() {
6161
<Text className="text-[17px] font-semibold text-white">Continue with Cal.com</Text>
6262
</TouchableOpacity>
6363

64-
{/* Sign up link */}
65-
<TouchableOpacity
66-
onPress={handleSignUp}
67-
className="mt-3 items-center justify-center py-1"
68-
style={Platform.OS === "web" ? { cursor: "pointer" } : undefined}
69-
activeOpacity={0.7}
70-
>
71-
<View>
72-
<Text className="text-[15px] text-gray-500">
73-
Don't have an account? <Text className="font-semibold text-gray-900">Sign up</Text>
74-
</Text>
75-
<View className="h-px bg-gray-400" style={{ marginTop: 2 }} />
76-
</View>
77-
</TouchableOpacity>
64+
{/* Sign up link - hidden on iOS */}
65+
{Platform.OS !== "ios" && (
66+
<TouchableOpacity
67+
onPress={handleSignUp}
68+
className="mt-3 items-center justify-center py-1"
69+
style={Platform.OS === "web" ? { cursor: "pointer" } : undefined}
70+
activeOpacity={0.7}
71+
>
72+
<View>
73+
<Text className="text-[15px] text-gray-500">
74+
Don't have an account? <Text className="font-semibold text-gray-900">Sign up</Text>
75+
</Text>
76+
<View className="h-px bg-gray-400" style={{ marginTop: 2 }} />
77+
</View>
78+
</TouchableOpacity>
79+
)}
7880
</View>
7981
</View>
8082
);

companion/components/event-type-detail/tabs/AdvancedTab.tsx

Lines changed: 75 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@ interface AdvancedTabProps {
246246
setAllowReschedulingPastEvents: (value: boolean) => void;
247247
allowBookingThroughRescheduleLink: boolean;
248248
setAllowBookingThroughRescheduleLink: (value: boolean) => void;
249+
redirectEnabled: boolean;
250+
setRedirectEnabled: (value: boolean) => void;
249251
successRedirectUrl: string;
250252
setSuccessRedirectUrl: (value: string) => void;
251253
forwardParamsSuccessRedirect: boolean;
@@ -271,6 +273,8 @@ interface AdvancedTabProps {
271273
setDisableRescheduling: (value: boolean) => void;
272274
sendCalVideoTranscription: boolean;
273275
setSendCalVideoTranscription: (value: boolean) => void;
276+
interfaceLanguageEnabled: boolean;
277+
setInterfaceLanguageEnabled: (value: boolean) => void;
274278
interfaceLanguage: string;
275279
setInterfaceLanguage: (value: string) => void;
276280
showOptimizedSlots: boolean;
@@ -294,10 +298,19 @@ export function AdvancedTab(props: AdvancedTabProps) {
294298
title="Requires confirmation"
295299
description="The booking needs to be manually confirmed before it is pushed to your calendar and a confirmation is sent."
296300
value={props.requiresConfirmation}
297-
onValueChange={props.setRequiresConfirmation}
301+
onValueChange={(value) => {
302+
if (value && props.seatsEnabled) {
303+
Alert.alert(
304+
"Disable 'Offer seats' first",
305+
"You need to:\n1. Disable 'Offer seats' and Save\n2. Then enable 'Requires confirmation' and Save again"
306+
);
307+
return;
308+
}
309+
props.setRequiresConfirmation(value);
310+
}}
298311
/>
299312
<SettingRow
300-
title="Email verification"
313+
title="Booker email verification"
301314
description="To ensure booker's email verification before scheduling events."
302315
value={props.requiresBookerEmailVerification}
303316
onValueChange={props.setRequiresBookerEmailVerification}
@@ -385,7 +398,16 @@ export function AdvancedTab(props: AdvancedTabProps) {
385398
title="Offer seats"
386399
description="Offer seats for booking. This automatically disables guest & opt-in bookings."
387400
value={props.seatsEnabled}
388-
onValueChange={props.setSeatsEnabled}
401+
onValueChange={(value) => {
402+
if (value && props.requiresConfirmation) {
403+
Alert.alert(
404+
"Disable 'Requires confirmation' first",
405+
"You need to:\n1. Disable 'Requires confirmation' and Save\n2. Then enable 'Offer seats' and Save again"
406+
);
407+
return;
408+
}
409+
props.setSeatsEnabled(value);
410+
}}
389411
learnMoreUrl="https://cal.com/help/event-types/offer-seats"
390412
isLast
391413
/>
@@ -438,15 +460,24 @@ export function AdvancedTab(props: AdvancedTabProps) {
438460

439461
{/* Language */}
440462
<SettingsGroup header="Language">
441-
<NavigationRow
463+
<SettingRow
442464
isFirst
443-
isLast
444-
title="Interface Language"
445-
value={getLanguageLabel(props.interfaceLanguage)}
446-
onPress={() => setShowLanguagePicker(true)}
447-
options={interfaceLanguageOptions}
448-
onSelect={props.setInterfaceLanguage}
465+
title="Custom interface language"
466+
description="Override the default browser language for the booking page."
467+
value={props.interfaceLanguageEnabled}
468+
onValueChange={props.setInterfaceLanguageEnabled}
469+
isLast={!props.interfaceLanguageEnabled}
449470
/>
471+
{props.interfaceLanguageEnabled ? (
472+
<NavigationRow
473+
isLast
474+
title="Select Language"
475+
value={getLanguageLabel(props.interfaceLanguage)}
476+
onPress={() => setShowLanguagePicker(true)}
477+
options={interfaceLanguageOptions}
478+
onSelect={props.setInterfaceLanguage}
479+
/>
480+
) : null}
450481
</SettingsGroup>
451482

452483
{/* Language Picker Modal */}
@@ -527,34 +558,42 @@ export function AdvancedTab(props: AdvancedTabProps) {
527558

528559
{/* Redirect */}
529560
<SettingsGroup header="Redirect">
530-
<View className="bg-white pl-4">
531-
<View className="border-b border-[#E5E5E5] pt-4 pb-3 pr-4">
532-
<Text className="mb-2 text-[13px] text-[#6D6D72]">
533-
Redirect URL after successful booking
534-
</Text>
535-
<TextInput
536-
className="rounded-lg bg-[#F2F2F7] px-3 py-2 text-[17px] text-black"
537-
value={props.successRedirectUrl}
538-
onChangeText={props.setSuccessRedirectUrl}
539-
placeholder="https://example.com/thank-you"
540-
placeholderTextColor="#8E8E93"
541-
keyboardType="url"
542-
autoCapitalize="none"
543-
/>
544-
{props.successRedirectUrl ? (
545-
<Text className="mt-2 text-[13px] text-[#FF9500]">
546-
Adding a redirect will disable the success page.
547-
</Text>
548-
) : null}
549-
</View>
550-
</View>
551561
<SettingRow
552-
title="Forward parameters"
553-
description="Forward parameters such as ?email=...&name=... to the redirect URL."
554-
value={props.forwardParamsSuccessRedirect}
555-
onValueChange={props.setForwardParamsSuccessRedirect}
556-
isLast
562+
isFirst
563+
title="Redirect on booking"
564+
description="Redirect to a custom URL after a successful booking."
565+
value={props.redirectEnabled}
566+
onValueChange={props.setRedirectEnabled}
567+
isLast={!props.redirectEnabled}
557568
/>
569+
{props.redirectEnabled ? (
570+
<>
571+
<View className="bg-white pl-4">
572+
<View className="border-b border-[#E5E5E5] pt-4 pb-3 pr-4">
573+
<Text className="mb-2 text-[13px] text-[#6D6D72]">Redirect URL</Text>
574+
<TextInput
575+
className="rounded-lg bg-[#F2F2F7] px-3 py-2 text-[17px] text-black"
576+
value={props.successRedirectUrl}
577+
onChangeText={props.setSuccessRedirectUrl}
578+
placeholder="https://example.com/thank-you"
579+
placeholderTextColor="#8E8E93"
580+
keyboardType="url"
581+
autoCapitalize="none"
582+
/>
583+
<Text className="mt-2 text-[13px] text-[#FF9500]">
584+
Adding a redirect will disable the success page.
585+
</Text>
586+
</View>
587+
</View>
588+
<SettingRow
589+
title="Forward parameters"
590+
description="Forward parameters such as ?email=...&name=... to the redirect URL."
591+
value={props.forwardParamsSuccessRedirect}
592+
onValueChange={props.setForwardParamsSuccessRedirect}
593+
isLast
594+
/>
595+
</>
596+
) : null}
558597
</SettingsGroup>
559598

560599
{/* Configure on Web Section */}

companion/components/event-type-detail/tabs/AvailabilityTab.tsx

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,17 @@ import { Platform, Text, TouchableOpacity, View } from "react-native";
1010
import type { Schedule } from "@/services/calcom";
1111
import { AvailabilityTabIOSPicker } from "./AvailabilityTabIOSPicker";
1212

13+
interface TimeSlot {
14+
startTime?: string;
15+
endTime?: string;
16+
}
17+
1318
interface DaySchedule {
1419
day: string;
1520
available: boolean;
1621
startTime?: string;
1722
endTime?: string;
23+
timeSlots?: TimeSlot[];
1824
}
1925

2026
interface AvailabilityTabProps {
@@ -181,6 +187,7 @@ export function AvailabilityTab(props: AvailabilityTabProps) {
181187
const dayInfo = daySchedules.find((d) => d.day === day);
182188
const isEnabled = dayInfo?.available ?? false;
183189
const isLast = index === DAYS.length - 1;
190+
const timeSlots = dayInfo?.timeSlots || [];
184191

185192
return (
186193
<View
@@ -203,16 +210,25 @@ export function AvailabilityTab(props: AvailabilityTabProps) {
203210
{day}
204211
</Text>
205212

206-
{/* Time range or Unavailable */}
207-
<Text
208-
className={`flex-1 text-right text-[15px] ${
209-
isEnabled ? "text-black" : "text-[#8E8E93]"
210-
}`}
211-
>
212-
{isEnabled && dayInfo?.startTime && dayInfo?.endTime
213-
? `${formatTime12Hour(dayInfo.startTime)} - ${formatTime12Hour(dayInfo.endTime)}`
214-
: "Unavailable"}
215-
</Text>
213+
{/* Time ranges or Unavailable - support multiple time slots */}
214+
{isEnabled && timeSlots.length > 0 ? (
215+
<View className="flex-1 items-end">
216+
{timeSlots.map((slot, slotIndex) => (
217+
<Text
218+
key={`${slotIndex}-${slot.startTime}`}
219+
className={`text-[15px] text-black ${slotIndex > 0 ? "mt-1" : ""}`}
220+
>
221+
{slot.startTime && slot.endTime
222+
? `${formatTime12Hour(slot.startTime)} - ${formatTime12Hour(slot.endTime)}`
223+
: ""}
224+
</Text>
225+
))}
226+
</View>
227+
) : (
228+
<Text className="flex-1 text-right text-[15px] text-[#8E8E93]">
229+
Unavailable
230+
</Text>
231+
)}
216232
</View>
217233
);
218234
})}

0 commit comments

Comments
 (0)