Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 38 additions & 20 deletions companion/app/(tabs)/(event-types)/event-type-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
};
});

Expand Down Expand Up @@ -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" ? (
Expand Down Expand Up @@ -2161,6 +2175,8 @@ export default function EventTypeDetail() {
setAllowReschedulingPastEvents={setAllowReschedulingPastEvents}
allowBookingThroughRescheduleLink={allowBookingThroughRescheduleLink}
setAllowBookingThroughRescheduleLink={setAllowBookingThroughRescheduleLink}
redirectEnabled={redirectEnabled}
setRedirectEnabled={setRedirectEnabled}
successRedirectUrl={successRedirectUrl}
setSuccessRedirectUrl={setSuccessRedirectUrl}
forwardParamsSuccessRedirect={forwardParamsSuccessRedirect}
Expand Down Expand Up @@ -2189,6 +2205,8 @@ export default function EventTypeDetail() {
setDisableRescheduling={setDisableRescheduling}
sendCalVideoTranscription={sendCalVideoTranscription}
setSendCalVideoTranscription={setSendCalVideoTranscription}
interfaceLanguageEnabled={interfaceLanguageEnabled}
setInterfaceLanguageEnabled={setInterfaceLanguageEnabled}
interfaceLanguage={interfaceLanguage}
setInterfaceLanguage={setInterfaceLanguage}
showOptimizedSlots={showOptimizedSlots}
Expand Down
30 changes: 16 additions & 14 deletions companion/components/LoginScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
);
Expand Down
111 changes: 75 additions & 36 deletions companion/components/event-type-detail/tabs/AdvancedTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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}
Expand Down Expand Up @@ -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
/>
Expand Down Expand Up @@ -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 */}
Expand Down Expand Up @@ -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 */}
Expand Down
36 changes: 26 additions & 10 deletions companion/components/event-type-detail/tabs/AvailabilityTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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>
);
})}
Expand Down
Loading
Loading