feat: companion add missing advanced event type (#26594)

* feat: companion add missing advanced event type

* feat: companion add missing advanced event type

* remove metadata code and remove non implemented Auto translate title and description toggle from companion

* Save Button Dirty State

* address cubic feedback - default value for sendTranscriptionEmails

* address cubic comments - fix new event type blocked issue

* ios language dropdown
This commit is contained in:
Dhairyashil Shinde authored and GitHub committed 2026-01-09 08:09:46 -03:00
1 parent 51639e38d7
commit d865f6cb5f
10 files changed
+623 -253

No files matched your search

+1
View File
@@ -32,6 +32,7 @@ yarn-error.*
# macOS
.DS_Store
*.pem
safari-extension/
# local env files
.env*.local
@@ -1,5 +1,4 @@
import { Stack } from "expo-router";
import { Platform } from "react-native";
export default function AvailabilityLayout() {
return (
@@ -107,12 +107,7 @@ export default function AvailabilityDetailIOS() {
</Stack.Header.Right>
</Stack.Header>
<AvailabilityDetailScreen
ref={screenRef}
id={id}
// @ts-expect-error - onActionsReady is only available in AvailabilityDetailScreen.ios.tsx
onActionsReady={handleActionsReady}
/>
<AvailabilityDetailScreen ref={screenRef} id={id} onActionsReady={handleActionsReady} />
</>
);
}
@@ -1,7 +1,7 @@
import { Ionicons } from "@expo/vector-icons";
import * as Clipboard from "expo-clipboard";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import { Activity, useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
Alert,
Animated,
@@ -49,10 +49,11 @@ import { isLiquidGlassAvailable } from "expo-glass-effect";
// Type definitions for extended EventType fields not in the base type
interface EventTypeExtended {
lengthInMinutesOptions?: number[];
disableCancelling?: boolean;
disableRescheduling?: boolean;
// API V2 format - these can be objects or booleans from older data
disableCancelling?: boolean | { disabled: boolean };
disableRescheduling?: boolean | { disabled: boolean; minutesBefore?: number };
sendCalVideoTranscription?: boolean;
autoTranslate?: boolean;
lockedTimeZone?: string;
hideCalendarEventDetails?: boolean;
hideOrganizerEmail?: boolean;
@@ -60,6 +61,12 @@ interface EventTypeExtended {
lightThemeHex?: string;
darkThemeHex?: string;
};
// API V2 new fields
calVideoSettings?: {
sendTranscriptionEmails?: boolean;
};
interfaceLanguage?: string;
showOptimizedSlots?: boolean;
}
interface BookerActiveBookingsLimitExtended {
@@ -199,8 +206,8 @@ export default function EventTypeDetail() {
const [requiresConfirmation, setRequiresConfirmation] = useState(false);
const [disableCancelling, setDisableCancelling] = useState(false);
const [disableRescheduling, setDisableRescheduling] = useState(false);
const [sendCalVideoTranscription, setSendCalVideoTranscription] = useState(false);
const [autoTranslate, setAutoTranslate] = useState(false);
const [sendCalVideoTranscription, setSendCalVideoTranscription] = useState(true);
const [requiresBookerEmailVerification, setRequiresBookerEmailVerification] = useState(false);
const [hideCalendarNotes, setHideCalendarNotes] = useState(false);
const [hideCalendarEventDetails, setHideCalendarEventDetails] = useState(false);
@@ -215,6 +222,8 @@ export default function EventTypeDetail() {
const [customReplyToEmail, setCustomReplyToEmail] = useState("");
const [eventTypeColorLight, setEventTypeColorLight] = useState("#292929");
const [eventTypeColorDark, setEventTypeColorDark] = useState("#FAFAFA");
const [interfaceLanguage, setInterfaceLanguage] = useState("");
const [showOptimizedSlots, setShowOptimizedSlots] = useState(false);
const [seatsEnabled, setSeatsEnabled] = useState(false);
const [seatsPerTimeSlot, setSeatsPerTimeSlot] = useState("2");
@@ -594,28 +603,55 @@ export default function EventTypeDetail() {
const metadata = eventType.metadata;
if (eventTypeExt.disableCancelling !== undefined) {
setDisableCancelling(eventTypeExt.disableCancelling);
// Handle disableCancelling - can be boolean or object { disabled: boolean }
const disableCancellingValue = eventTypeExt.disableCancelling;
if (disableCancellingValue !== undefined) {
if (typeof disableCancellingValue === "boolean") {
setDisableCancelling(disableCancellingValue);
} else if (
typeof disableCancellingValue === "object" &&
"disabled" in disableCancellingValue
) {
setDisableCancelling(disableCancellingValue.disabled);
}
} else if (metadata?.disableCancelling) {
setDisableCancelling(true);
}
if (eventTypeExt.disableRescheduling !== undefined) {
setDisableRescheduling(eventTypeExt.disableRescheduling);
// Handle disableRescheduling - can be boolean or object { disabled: boolean, minutesBefore?: number }
const disableReschedulingValue = eventTypeExt.disableRescheduling;
if (disableReschedulingValue !== undefined) {
if (typeof disableReschedulingValue === "boolean") {
setDisableRescheduling(disableReschedulingValue);
} else if (
typeof disableReschedulingValue === "object" &&
"disabled" in disableReschedulingValue
) {
setDisableRescheduling(disableReschedulingValue.disabled);
}
} else if (metadata?.disableRescheduling) {
setDisableRescheduling(true);
}
if (eventTypeExt.sendCalVideoTranscription !== undefined) {
// Handle calVideoSettings.sendTranscriptionEmails (API V2) or sendCalVideoTranscription (legacy)
if (eventTypeExt.calVideoSettings?.sendTranscriptionEmails !== undefined) {
setSendCalVideoTranscription(eventTypeExt.calVideoSettings.sendTranscriptionEmails);
} else if (eventTypeExt.sendCalVideoTranscription !== undefined) {
setSendCalVideoTranscription(eventTypeExt.sendCalVideoTranscription);
} else if (metadata?.sendCalVideoTranscription) {
setSendCalVideoTranscription(true);
} else {
setSendCalVideoTranscription(false);
}
if (eventTypeExt.autoTranslate !== undefined) {
setAutoTranslate(eventTypeExt.autoTranslate);
} else if (metadata?.autoTranslate) {
setAutoTranslate(true);
// Load interface language (API V2)
if (eventTypeExt.interfaceLanguage !== undefined) {
setInterfaceLanguage(eventTypeExt.interfaceLanguage);
}
// Load showOptimizedSlots (API V2)
if (eventTypeExt.showOptimizedSlots !== undefined) {
setShowOptimizedSlots(eventTypeExt.showOptimizedSlots);
}
if (metadata) {
@@ -940,6 +976,171 @@ export default function EventTypeDetail() {
]);
};
// Calculate current form state efficiently
const currentFormState = useMemo(
() => ({
// Basics
eventTitle,
eventSlug,
eventDescription,
eventDuration,
isHidden,
locations,
disableGuests,
// Multiple durations
allowMultipleDurations,
selectedDurations,
defaultDuration,
// Availability
selectedScheduleId: selectedSchedule?.id,
// Limits
beforeEventBuffer,
afterEventBuffer,
minimumNoticeValue,
minimumNoticeUnit,
slotInterval,
limitBookingFrequency,
frequencyLimits,
limitTotalDuration,
durationLimits,
onlyShowFirstAvailableSlot,
maxActiveBookingsPerBooker,
maxActiveBookingsValue,
offerReschedule,
limitFutureBookings,
futureBookingType,
rollingDays,
rollingCalendarDays,
rangeStartDate,
rangeEndDate,
// Advanced
requiresConfirmation,
requiresBookerEmailVerification,
hideCalendarNotes,
hideCalendarEventDetails,
hideOrganizerEmail,
lockTimezone,
allowReschedulingPastEvents,
allowBookingThroughRescheduleLink,
successRedirectUrl,
forwardParamsSuccessRedirect,
customReplyToEmail,
eventTypeColorLight,
eventTypeColorDark,
calendarEventName,
addToCalendarEmail,
selectedLayouts,
defaultLayout,
disableCancelling,
disableRescheduling,
sendCalVideoTranscription,
interfaceLanguage,
showOptimizedSlots,
// Seats
seatsEnabled,
seatsPerTimeSlot,
showAttendeeInfo,
showAvailabilityCount,
// Recurring
recurringEnabled,
recurringInterval,
recurringFrequency,
recurringOccurrences,
}),
[
eventTitle,
eventSlug,
eventDescription,
eventDuration,
isHidden,
locations,
disableGuests,
allowMultipleDurations,
selectedDurations,
defaultDuration,
selectedSchedule,
beforeEventBuffer,
afterEventBuffer,
minimumNoticeValue,
minimumNoticeUnit,
slotInterval,
limitBookingFrequency,
frequencyLimits,
limitTotalDuration,
durationLimits,
onlyShowFirstAvailableSlot,
maxActiveBookingsPerBooker,
maxActiveBookingsValue,
offerReschedule,
limitFutureBookings,
futureBookingType,
rollingDays,
rollingCalendarDays,
rangeStartDate,
rangeEndDate,
requiresConfirmation,
requiresBookerEmailVerification,
hideCalendarNotes,
hideCalendarEventDetails,
hideOrganizerEmail,
lockTimezone,
allowReschedulingPastEvents,
allowBookingThroughRescheduleLink,
successRedirectUrl,
forwardParamsSuccessRedirect,
customReplyToEmail,
eventTypeColorLight,
eventTypeColorDark,
calendarEventName,
addToCalendarEmail,
selectedLayouts,
defaultLayout,
disableCancelling,
disableRescheduling,
sendCalVideoTranscription,
interfaceLanguage,
showOptimizedSlots,
seatsEnabled,
seatsPerTimeSlot,
showAttendeeInfo,
showAvailabilityCount,
recurringEnabled,
recurringInterval,
recurringFrequency,
recurringOccurrences,
]
);
// Calculate isDirty state
const isDirty = useMemo(() => {
// For new event types (id="new"), we always want to enable save if required fields are filled
// But for now, let's stick to the dirty check logic which applies mostly to updates
if (id === "new") {
// For create mode, we can consider it "dirty" if title and slug are present
// or just always enabled. The requirement was "when user changes something".
// Usually for "new" forms, the button IS enabled, or disabled until valid.
// Let's assume the requirement "enable/disable based on changes" specifically targets the EDIT flow.
// But to be safe and consistent with "disable until change", we can check if it differs from initial empty state?
// Actually, for "Create", it's usually better to be enabled or validated.
// However, the user request: "we have this save button in event types, so when user changes something then only i want this save button to enable"
// This strongly implies the Edit case.
// For "new", returning true (always dirty/ready) is a safe default to avoid blocking creation.
return true;
}
if (!eventTypeData) return false;
const payload = buildPartialUpdatePayload(currentFormState, eventTypeData);
return Object.keys(payload).length > 0;
}, [currentFormState, eventTypeData, id]);
const handleSave = async () => {
if (!id) {
Alert.alert("Error", "Event type ID is missing");
@@ -1017,88 +1218,14 @@ export default function EventTypeDetail() {
setSaving(false);
} else {
// For UPDATE mode, use partial update - only send changed fields
const currentFormState = {
// Basics
eventTitle,
eventSlug,
eventDescription,
eventDuration,
isHidden,
locations,
disableGuests,
// Multiple durations
allowMultipleDurations,
selectedDurations,
defaultDuration,
// Availability
selectedScheduleId,
// Limits
beforeEventBuffer,
afterEventBuffer,
minimumNoticeValue,
minimumNoticeUnit,
slotInterval,
limitBookingFrequency,
frequencyLimits,
limitTotalDuration,
durationLimits,
onlyShowFirstAvailableSlot,
maxActiveBookingsPerBooker,
maxActiveBookingsValue,
offerReschedule,
limitFutureBookings,
futureBookingType,
rollingDays,
rollingCalendarDays,
rangeStartDate,
rangeEndDate,
// Advanced
requiresConfirmation,
requiresBookerEmailVerification,
hideCalendarNotes,
hideCalendarEventDetails,
hideOrganizerEmail,
lockTimezone,
allowReschedulingPastEvents,
allowBookingThroughRescheduleLink,
successRedirectUrl,
forwardParamsSuccessRedirect,
customReplyToEmail,
eventTypeColorLight,
eventTypeColorDark,
calendarEventName,
addToCalendarEmail,
selectedLayouts,
defaultLayout,
disableCancelling,
disableRescheduling,
sendCalVideoTranscription,
autoTranslate,
// Seats
seatsEnabled,
seatsPerTimeSlot,
showAttendeeInfo,
showAvailabilityCount,
// Recurring
recurringEnabled,
recurringInterval,
recurringFrequency,
recurringOccurrences,
};
// Build partial payload with only changed fields
// Using the memoized currentFormState
const payload = buildPartialUpdatePayload(currentFormState, eventTypeData);
if (Object.keys(payload).length === 0) {
Alert.alert("No Changes", "No changes were made to the event type.");
setSaving(false);
return;
// This should theoretically strictly not be reached if button is disabled,
// but it acts as a safeguard.
// setSaving(false);
// return;
}
try {
@@ -1179,10 +1306,16 @@ export default function EventTypeDetail() {
{/* Save Button */}
<AppPressable
onPress={handleSave}
disabled={saving}
className={`px-2 py-2 ${saving ? "opacity-50" : ""}`}
disabled={saving || !isDirty}
className={`px-2 py-2 ${saving || !isDirty ? "opacity-50" : ""}`}
>
<Text className="text-[16px] font-semibold text-[#007AFF]">{saveButtonText}</Text>
<Text
className={`text-[16px] font-semibold ${
saving || !isDirty ? "text-[#C7C7CC]" : "text-[#007AFF]"
}`}
>
{saveButtonText}
</Text>
</AppPressable>
</View>
</HeaderButtonWrapper>
@@ -1238,7 +1371,7 @@ export default function EventTypeDetail() {
</Stack.Header.Menu>
<Stack.Header.Button
onPress={handleSave}
disabled={saving}
disabled={saving || !isDirty}
variant="prominent"
tintColor="#000"
>
@@ -1859,8 +1992,6 @@ export default function EventTypeDetail() {
<AdvancedTab
requiresConfirmation={requiresConfirmation}
setRequiresConfirmation={setRequiresConfirmation}
autoTranslate={autoTranslate}
setAutoTranslate={setAutoTranslate}
requiresBookerEmailVerification={requiresBookerEmailVerification}
setRequiresBookerEmailVerification={setRequiresBookerEmailVerification}
hideCalendarNotes={hideCalendarNotes}
@@ -1898,6 +2029,17 @@ export default function EventTypeDetail() {
setShowAvailabilityCount={setShowAvailabilityCount}
// Event type ID for private links
eventTypeId={id}
// New API V2 props
disableCancelling={disableCancelling}
setDisableCancelling={setDisableCancelling}
disableRescheduling={disableRescheduling}
setDisableRescheduling={setDisableRescheduling}
sendCalVideoTranscription={sendCalVideoTranscription}
setSendCalVideoTranscription={setSendCalVideoTranscription}
interfaceLanguage={interfaceLanguage}
setInterfaceLanguage={setInterfaceLanguage}
showOptimizedSlots={showOptimizedSlots}
setShowOptimizedSlots={setShowOptimizedSlots}
/>
) : null}
@@ -1,8 +1,63 @@
import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect";
import { Stack } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
import { Alert, Switch, Text, TextInput, TouchableOpacity, View } from "react-native";
import { useState } from "react";
import {
Alert,
Modal,
Platform,
ScrollView,
Switch,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { openInAppBrowser } from "@/utils/browser";
// Interface language options matching API V2 enum
const interfaceLanguageOptions = [
{ label: "Browser Default", value: "" },
{ label: "English", value: "en" },
{ label: "العربية", value: "ar" },
{ label: "Azərbaycan", value: "az" },
{ label: "Български", value: "bg" },
{ label: "বাংলা", value: "bn" },
{ label: "Català", value: "ca" },
{ label: "Čeština", value: "cs" },
{ label: "Dansk", value: "da" },
{ label: "Deutsch", value: "de" },
{ label: "Ελληνικά", value: "el" },
{ label: "Español", value: "es" },
{ label: "Español latinoamericano", value: "es-419" },
{ label: "Eesti", value: "et" },
{ label: "Euskara", value: "eu" },
{ label: "Suomi", value: "fi" },
{ label: "Français", value: "fr" },
{ label: "עברית", value: "he" },
{ label: "Magyar", value: "hu" },
{ label: "Italiano", value: "it" },
{ label: "日本語", value: "ja" },
{ label: "ខ្មែរ", value: "km" },
{ label: "한국어", value: "ko" },
{ label: "Nederlands", value: "nl" },
{ label: "Norsk", value: "no" },
{ label: "Polski", value: "pl" },
{ label: "Português", value: "pt" },
{ label: "Português (Brasil)", value: "pt-BR" },
{ label: "Română", value: "ro" },
{ label: "Русский", value: "ru" },
{ label: "Slovenčina (Slovensko)", value: "sk-SK" },
{ label: "Српски", value: "sr" },
{ label: "Svenska", value: "sv" },
{ label: "Türkçe", value: "tr" },
{ label: "Українська", value: "uk" },
{ label: "Tiếng Việt", value: "vi" },
{ label: "中文(中国)", value: "zh-CN" },
{ label: "中文(台灣)", value: "zh-TW" },
];
interface ConfigureOnWebCardProps {
title: string;
description: string;
@@ -43,8 +98,7 @@ function ConfigureOnWebCard({
interface AdvancedTabProps {
requiresConfirmation: boolean;
setRequiresConfirmation: (value: boolean) => void;
autoTranslate: boolean;
setAutoTranslate: (value: boolean) => void;
requiresBookerEmailVerification: boolean;
setRequiresBookerEmailVerification: (value: boolean) => void;
hideCalendarNotes: boolean;
@@ -80,9 +134,27 @@ interface AdvancedTabProps {
showAvailabilityCount: boolean;
setShowAvailabilityCount: (value: boolean) => void;
eventTypeId: string;
// New API V2 props
disableCancelling: boolean;
setDisableCancelling: (value: boolean) => void;
disableRescheduling: boolean;
setDisableRescheduling: (value: boolean) => void;
sendCalVideoTranscription: boolean;
setSendCalVideoTranscription: (value: boolean) => void;
interfaceLanguage: string;
setInterfaceLanguage: (value: string) => void;
showOptimizedSlots: boolean;
setShowOptimizedSlots: (value: boolean) => void;
}
export function AdvancedTab(props: AdvancedTabProps) {
const [showLanguagePicker, setShowLanguagePicker] = useState(false);
const getLanguageLabel = (value: string) => {
const option = interfaceLanguageOptions.find((opt) => opt.value === value);
return option?.label || "Browser Default";
};
return (
<View className="gap-3">
<View className="rounded-2xl border border-[#E5E5EA] bg-white p-5">
@@ -103,53 +175,192 @@ export function AdvancedTab(props: AdvancedTabProps) {
</View>
</View>
<ConfigureOnWebCard
title="Disable Cancelling"
description="Guests and Organizer can no longer cancel the event with calendar invite or email."
eventTypeId={props.eventTypeId}
browserTitle="Disable Cancelling"
/>
<ConfigureOnWebCard
title="Disable Rescheduling"
description="Guests and Organizer can no longer reschedule the event with calendar invite or email."
eventTypeId={props.eventTypeId}
browserTitle="Disable Rescheduling"
/>
<ConfigureOnWebCard
title="Send Cal Video Transcription Emails"
description="Send emails with the transcription of the Cal Video after the meeting ends. (Requires a paid plan)"
eventTypeId={props.eventTypeId}
browserTitle="Cal Video Transcription"
/>
<View className="rounded-2xl border border-[#E5E5EA] bg-white p-5">
<View className="flex-row items-start justify-between">
<View className="mr-3 flex-1">
<Text className="mb-1 text-base font-medium text-[#333]">
Auto translate title and description
</Text>
<Text className="mb-1 text-base font-medium text-[#333]">Disable Cancelling</Text>
<Text className="text-sm text-[#666]">
Automatically translate titles and descriptions to the visitor's browser language
using AI.
Guests and Organizer can no longer cancel the event with calendar invite or email.
</Text>
</View>
<Switch
value={props.autoTranslate}
onValueChange={props.setAutoTranslate}
value={props.disableCancelling}
onValueChange={props.setDisableCancelling}
trackColor={{ false: "#E5E5EA", true: "#34C759" }}
thumbColor="#FFFFFF"
/>
</View>
</View>
<ConfigureOnWebCard
title="Interface Language"
description="Set your preferred language for the booking interface."
eventTypeId={props.eventTypeId}
browserTitle="Interface Language"
/>
<View className="rounded-2xl border border-[#E5E5EA] bg-white p-5">
<View className="flex-row items-start justify-between">
<View className="mr-3 flex-1">
<Text className="mb-1 text-base font-medium text-[#333]">Disable Rescheduling</Text>
<Text className="text-sm text-[#666]">
Guests and Organizer can no longer reschedule the event with calendar invite or email.
</Text>
</View>
<Switch
value={props.disableRescheduling}
onValueChange={props.setDisableRescheduling}
trackColor={{ false: "#E5E5EA", true: "#34C759" }}
thumbColor="#FFFFFF"
/>
</View>
</View>
<View className="rounded-2xl border border-[#E5E5EA] bg-white p-5">
<View className="flex-row items-start justify-between">
<View className="mr-3 flex-1">
<Text className="mb-1 text-base font-medium text-[#333]">
Send Cal Video Transcription Emails
</Text>
<Text className="text-sm text-[#666]">
Send emails with the transcription of the Cal Video after the meeting ends.
</Text>
</View>
<Switch
value={props.sendCalVideoTranscription}
onValueChange={props.setSendCalVideoTranscription}
trackColor={{ false: "#E5E5EA", true: "#34C759" }}
thumbColor="#FFFFFF"
/>
</View>
</View>
<View className="rounded-2xl border border-[#E5E5EA] bg-white p-5">
<Text className="mb-1.5 text-base font-medium text-[#333]">Interface Language</Text>
<Text className="mb-3 text-sm text-[#666]">
Set your preferred language for the booking interface.
</Text>
<TouchableOpacity
className="flex-row items-center justify-between rounded-lg border border-[#E5E5EA] bg-[#F8F9FA] px-3 py-3"
onPress={() => setShowLanguagePicker(true)}
>
<Text className="text-base text-[#333]">{getLanguageLabel(props.interfaceLanguage)}</Text>
<Ionicons name="chevron-down" size={20} color="#666" />
</TouchableOpacity>
{/* Language Picker Modal */}
<Modal
visible={showLanguagePicker}
animationType="slide"
transparent={Platform.OS !== "ios"}
presentationStyle={Platform.OS === "ios" ? "formSheet" : undefined}
onRequestClose={() => setShowLanguagePicker(false)}
>
{Platform.OS === "ios" ? (
<View
style={{
flex: 1,
backgroundColor: isLiquidGlassAvailable() ? "transparent" : "#F2F2F7",
}}
>
{/* Glass Header */}
{isLiquidGlassAvailable() ? (
<GlassView
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: 60, // Standard header height
zIndex: 10,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 16,
}}
glassEffectStyle="regular"
>
<TouchableOpacity
onPress={() => setShowLanguagePicker(false)}
className="h-[30px] w-[30px] items-center justify-center rounded-full bg-[#E5E5EA]"
>
<Ionicons name="close" size={20} color="#8E8E93" />
</TouchableOpacity>
<Text className="text-[17px] font-semibold text-black">Select Language</Text>
{/* Spacer to balance the centered title since we removed the right button */}
<View className="h-8 w-8" />
</GlassView>
) : (
<View className="absolute left-0 right-0 top-0 z-10 h-[60px] flex-row items-center justify-between border-b border-[#E5E5EA] bg-white px-4">
<TouchableOpacity
onPress={() => setShowLanguagePicker(false)}
className="h-8 w-8 items-center justify-center rounded-full bg-[#E5E5EA]"
>
<Ionicons name="close" size={20} color="#8E8E93" />
</TouchableOpacity>
<Text className="text-[17px] font-semibold text-black">Select Language</Text>
{/* Spacer to balance the centered title */}
<View className="h-8 w-8" />
</View>
)}
<ScrollView
className="flex-1"
contentContainerStyle={{ paddingTop: 80, paddingHorizontal: 16, paddingBottom: 40 }}
>
<View className="overflow-hidden rounded-xl border border-gray-300/40 bg-white/60">
{interfaceLanguageOptions.map((option, index) => (
<TouchableOpacity
key={option.value}
className={`flex-row items-center justify-between px-4 py-3 active:bg-white/80 ${
index !== interfaceLanguageOptions.length - 1
? "border-b border-gray-300/40"
: ""
}`}
onPress={() => {
props.setInterfaceLanguage(option.value);
setShowLanguagePicker(false);
}}
>
<Text className="text-base text-[#333]">{option.label}</Text>
{props.interfaceLanguage === option.value ? (
<Ionicons name="checkmark" size={20} color="#007AFF" />
) : null}
</TouchableOpacity>
))}
</View>
</ScrollView>
</View>
) : (
<View className="flex-1 justify-end bg-black/50">
<View className="max-h-[70%] rounded-t-3xl bg-white">
<View className="flex-row items-center justify-between border-b border-[#E5E5EA] p-4">
<Text className="text-lg font-semibold text-[#333]">Select Language</Text>
<TouchableOpacity onPress={() => setShowLanguagePicker(false)}>
<Ionicons name="close" size={24} color="#666" />
</TouchableOpacity>
</View>
<ScrollView className="p-2">
{interfaceLanguageOptions.map((option) => (
<TouchableOpacity
key={option.value}
className={`flex-row items-center justify-between rounded-lg p-4 ${
props.interfaceLanguage === option.value ? "bg-[#F0F0F0]" : ""
}`}
onPress={() => {
props.setInterfaceLanguage(option.value);
setShowLanguagePicker(false);
}}
>
<Text className="text-base text-[#333]">{option.label}</Text>
{props.interfaceLanguage === option.value ? (
<Ionicons name="checkmark" size={20} color="#007AFF" />
) : null}
</TouchableOpacity>
))}
</ScrollView>
</View>
</View>
)}
</Modal>
</View>
<View className="rounded-2xl border border-[#E5E5EA] bg-white p-5">
<View className="flex-row items-start justify-between">
@@ -425,19 +636,44 @@ export function AdvancedTab(props: AdvancedTabProps) {
) : null}
</View>
<ConfigureOnWebCard
title="Allow rescheduling past events"
description="Enabling this option allows for past events to be rescheduled."
eventTypeId={props.eventTypeId}
browserTitle="Allow Rescheduling Past Events"
/>
<View className="rounded-2xl border border-[#E5E5EA] bg-white p-5">
<View className="flex-row items-start justify-between">
<View className="mr-3 flex-1">
<Text className="mb-1 text-base font-medium text-[#333]">
Allow rescheduling past events
</Text>
<Text className="text-sm text-[#666]">
Enabling this option allows for past events to be rescheduled.
</Text>
</View>
<Switch
value={props.allowReschedulingPastEvents}
onValueChange={props.setAllowReschedulingPastEvents}
trackColor={{ false: "#E5E5EA", true: "#34C759" }}
thumbColor="#FFFFFF"
/>
</View>
</View>
<ConfigureOnWebCard
title="Allow booking through reschedule link"
description="When enabled, users will be able to create a new booking when trying to reschedule a cancelled booking."
eventTypeId={props.eventTypeId}
browserTitle="Allow Booking Through Reschedule Link"
/>
<View className="rounded-2xl border border-[#E5E5EA] bg-white p-5">
<View className="flex-row items-start justify-between">
<View className="mr-3 flex-1">
<Text className="mb-1 text-base font-medium text-[#333]">
Allow booking through reschedule link
</Text>
<Text className="text-sm text-[#666]">
When enabled, users will be able to create a new booking when trying to reschedule a
cancelled booking.
</Text>
</View>
<Switch
value={props.allowBookingThroughRescheduleLink}
onValueChange={props.setAllowBookingThroughRescheduleLink}
trackColor={{ false: "#E5E5EA", true: "#34C759" }}
thumbColor="#FFFFFF"
/>
</View>
</View>
<ConfigureOnWebCard
title="Custom 'Reply-To' email"
@@ -502,12 +738,22 @@ export function AdvancedTab(props: AdvancedTabProps) {
</View>
</View>
<ConfigureOnWebCard
title="Optimized slots"
description="Arrange time slots to optimize availability."
eventTypeId={props.eventTypeId}
browserTitle="Optimized Slots"
/>
<View className="rounded-2xl border border-[#E5E5EA] bg-white p-5">
<View className="flex-row items-start justify-between">
<View className="mr-3 flex-1">
<Text className="mb-1 text-base font-medium text-[#333]">Optimized slots</Text>
<Text className="text-sm text-[#666]">
Arrange time slots to optimize availability.
</Text>
</View>
<Switch
value={props.showOptimizedSlots}
onValueChange={props.setShowOptimizedSlots}
trackColor={{ false: "#E5E5EA", true: "#34C759" }}
thumbColor="#FFFFFF"
/>
</View>
</View>
</View>
);
}
@@ -124,7 +124,9 @@ interface EventTypeFormState {
disableCancelling: boolean;
disableRescheduling: boolean;
sendCalVideoTranscription: boolean;
autoTranslate: boolean;
interfaceLanguage: string;
showOptimizedSlots: boolean;
// Seats
seatsEnabled: boolean;
@@ -728,32 +730,43 @@ export function buildPartialUpdatePayload(
const metadataChanges: Record<string, unknown> = {};
const originalMetadata = original.metadata || {};
if (
currentState.disableCancelling !==
(originalMetadata.disableCancelling || original.disableCancelling || false)
) {
metadataChanges.disableCancelling = currentState.disableCancelling;
// Disable Cancelling
const originalDisableCancelling =
typeof original.disableCancelling === "object"
? original.disableCancelling.disabled
: original.disableCancelling || false;
if (currentState.disableCancelling !== originalDisableCancelling) {
payload.disableCancelling = { disabled: currentState.disableCancelling };
}
if (
currentState.disableRescheduling !==
(originalMetadata.disableRescheduling || original.disableRescheduling || false)
) {
metadataChanges.disableRescheduling = currentState.disableRescheduling;
// Disable Rescheduling
const originalDisableRescheduling =
typeof original.disableRescheduling === "object"
? original.disableRescheduling.disabled
: original.disableRescheduling || false;
if (currentState.disableRescheduling !== originalDisableRescheduling) {
payload.disableRescheduling = { disabled: currentState.disableRescheduling };
}
if (
currentState.sendCalVideoTranscription !==
(originalMetadata.sendCalVideoTranscription || original.sendCalVideoTranscription || false)
) {
metadataChanges.sendCalVideoTranscription = currentState.sendCalVideoTranscription;
// Cal Video Settings
const originalSendTranscription = original.calVideoSettings?.sendTranscriptionEmails ?? false;
if (currentState.sendCalVideoTranscription !== originalSendTranscription) {
payload.calVideoSettings = {
sendTranscriptionEmails: currentState.sendCalVideoTranscription,
};
}
if (
currentState.autoTranslate !==
(originalMetadata.autoTranslate || original.autoTranslate || false)
) {
metadataChanges.autoTranslate = currentState.autoTranslate;
// Interface Language (API V2)
if (currentState.interfaceLanguage !== (original.interfaceLanguage || "")) {
payload.interfaceLanguage = currentState.interfaceLanguage || null;
}
// Show Optimized Slots (API V2)
if (currentState.showOptimizedSlots !== (original.showOptimizedSlots || false)) {
payload.showOptimizedSlots = currentState.showOptimizedSlots;
}
if ((currentState.calendarEventName || "") !== (originalMetadata.calendarEventName || "")) {
@@ -39,38 +39,6 @@ const formatTime12Hour = (time24: string): string => {
return `${hour12Padded}:${min} ${period}`;
};
// Format availability for display - groups days with same time range
const formatAvailabilityDisplay = (
availability: Record<number, ScheduleAvailability[]>
): string[] => {
const timeRangeMap: Record<string, number[]> = {};
Object.keys(availability).forEach((dayIndexStr) => {
const dayIndex = Number(dayIndexStr);
const slots = availability[dayIndex];
if (slots && slots.length > 0) {
slots.forEach((slot) => {
const timeKey = `${slot.startTime}-${slot.endTime}`;
if (!timeRangeMap[timeKey]) {
timeRangeMap[timeKey] = [];
}
timeRangeMap[timeKey].push(dayIndex);
});
}
});
const formatted: string[] = [];
Object.keys(timeRangeMap).forEach((timeKey) => {
const days = timeRangeMap[timeKey].sort((a, b) => a - b);
const [startTime, endTime] = timeKey.split("-");
const dayNames = days.map((day) => DAYS[day]).join(", ");
const timeRange = `${formatTime12Hour(startTime)} - ${formatTime12Hour(endTime)}`;
formatted.push(`${dayNames}, ${timeRange}`);
});
return formatted;
};
// Format date for display
const formatDateForDisplay = (dateStr: string): string => {
if (!dateStr) return "";
@@ -39,38 +39,6 @@ const formatTime12Hour = (time24: string): string => {
return `${hour12Padded}:${min} ${period}`;
};
// Format availability for display - groups days with same time range
const formatAvailabilityDisplay = (
availability: Record<number, ScheduleAvailability[]>
): string[] => {
const timeRangeMap: Record<string, number[]> = {};
Object.keys(availability).forEach((dayIndexStr) => {
const dayIndex = Number(dayIndexStr);
const slots = availability[dayIndex];
if (slots && slots.length > 0) {
slots.forEach((slot) => {
const timeKey = `${slot.startTime}-${slot.endTime}`;
if (!timeRangeMap[timeKey]) {
timeRangeMap[timeKey] = [];
}
timeRangeMap[timeKey].push(dayIndex);
});
}
});
const formatted: string[] = [];
Object.keys(timeRangeMap).forEach((timeKey) => {
const days = timeRangeMap[timeKey].sort((a, b) => a - b);
const [startTime, endTime] = timeKey.split("-");
const dayNames = days.map((day) => DAYS[day]).join(", ");
const timeRange = `${formatTime12Hour(startTime)} - ${formatTime12Hour(endTime)}`;
formatted.push(`${dayNames}, ${timeRange}`);
});
return formatted;
};
// Format date for display
const formatDateForDisplay = (dateStr: string): string => {
if (!dateStr) return "";
+1
View File
@@ -1240,6 +1240,7 @@ function sanitizePayload(payload: Record<string, unknown>): Record<string, unkno
"slotInterval",
"eventName",
"timeZone",
"interfaceLanguage",
];
for (const [key, value] of Object.entries(payload)) {
+43 -6
View File
@@ -77,6 +77,29 @@ export interface EmailSettings {
additionalGuestsEmails?: string[];
}
// Disable Cancelling settings (API V2 format)
export interface DisableCancelling {
disabled: boolean;
}
// Disable Rescheduling settings (API V2 format)
export interface DisableRescheduling {
disabled: boolean;
minutesBefore?: number; // Optional: disable when less than X minutes before
}
// Cal Video Settings
export interface CalVideoSettings {
disableRecordingForOrganizer?: boolean;
disableRecordingForGuests?: boolean;
redirectUrlOnExit?: string;
enableAutomaticRecordingForOrganizer?: boolean;
enableAutomaticTranscription?: boolean;
disableTranscriptionForGuests?: boolean;
disableTranscriptionForOrganizer?: boolean;
sendTranscriptionEmails?: boolean;
}
// Booking Field Types
export interface BaseBookingField {
name: string;
@@ -231,23 +254,30 @@ export interface EventType {
// Metadata
metadata?: Record<string, unknown>;
// Booking action settings (may also be in metadata)
disableRescheduling?: boolean;
disableCancelling?: boolean;
// Booking action settings (API V2 format)
disableRescheduling?: DisableRescheduling;
disableCancelling?: DisableCancelling;
minimumRescheduleNotice?: number;
allowReschedulingPastBookings?: boolean;
allowReschedulingCancelledBookings?: boolean;
// Additional properties from API responses
hideCalendarEventDetails?: boolean;
hideOrganizerEmail?: boolean;
allowReschedulingCancelledBookings?: boolean;
customReplyToEmail?: string;
color?: {
lightThemeHex?: string;
darkThemeHex?: string;
};
sendCalVideoTranscription?: boolean;
autoTranslate?: boolean;
// Cal Video Settings
calVideoSettings?: CalVideoSettings;
// Interface language (API V2)
interfaceLanguage?: string;
// Optimized slots (API V2)
showOptimizedSlots?: boolean;
}
export interface CreateEventTypeInput {
@@ -309,6 +339,13 @@ export interface CreateEventTypeInput {
successRedirectUrl?: string;
forwardParamsSuccessRedirect?: boolean;
metadata?: Record<string, unknown>;
// API V2 new fields
disableCancelling?: DisableCancelling;
disableRescheduling?: DisableRescheduling;
calVideoSettings?: CalVideoSettings;
interfaceLanguage?: string;
showOptimizedSlots?: boolean;
}
export interface GetEventTypesResponse {