diff --git a/companion/app/(tabs)/(bookings)/_layout.tsx b/companion/app/(tabs)/(bookings)/_layout.tsx
index 0eef26ba20..972e613628 100644
--- a/companion/app/(tabs)/(bookings)/_layout.tsx
+++ b/companion/app/(tabs)/(bookings)/_layout.tsx
@@ -6,12 +6,6 @@ export default function BookingsLayout() {
-
-
-
-
-
-
);
}
diff --git a/companion/app/(tabs)/(event-types)/index.ios.tsx b/companion/app/(tabs)/(event-types)/index.ios.tsx
index c02ed5cc45..210db493a0 100644
--- a/companion/app/(tabs)/(event-types)/index.ios.tsx
+++ b/companion/app/(tabs)/(event-types)/index.ios.tsx
@@ -18,7 +18,6 @@ import {
} from "react-native";
import { EmptyScreen } from "@/components/EmptyScreen";
import { EventTypeListItem } from "@/components/event-type-list-item/EventTypeListItem";
-import { FullScreenModal } from "@/components/FullScreenModal";
import { LoadingSpinner } from "@/components/LoadingSpinner";
import {
useCreateEventType,
@@ -57,7 +56,7 @@ export default function EventTypesIOS() {
const refreshing = isFetching && !loading;
const { mutate: createEventTypeMutation } = useCreateEventType();
- const { mutate: deleteEventTypeMutation, isPending: isDeleting } = useDeleteEventType();
+ const { mutate: deleteEventTypeMutation } = useDeleteEventType();
const { mutate: duplicateEventTypeMutation } = useDuplicateEventType();
// Convert query error to string
@@ -69,9 +68,7 @@ export default function EventTypesIOS() {
queryError?.message?.includes("401");
const error = queryError && !isAuthError && __DEV__ ? "Failed to load event types." : null;
- // Modal state for delete confirmation
- const [showDeleteModal, setShowDeleteModal] = useState(false);
- const [eventTypeToDelete, setEventTypeToDelete] = useState(null);
+ // No modal state needed for iOS - using native Alert for delete confirmation
// Handle pull-to-refresh (offline-aware)
const onRefresh = () => offlineAwareRefresh(refetch);
@@ -164,33 +161,35 @@ export default function EventTypesIOS() {
};
const handleDelete = (eventType: EventType) => {
- setEventTypeToDelete(eventType);
- setShowDeleteModal(true);
- };
-
- const confirmDelete = () => {
- if (!eventTypeToDelete) return;
-
- deleteEventTypeMutation(eventTypeToDelete.id, {
- onSuccess: () => {
- // Close modal and reset state
- setShowDeleteModal(false);
- setEventTypeToDelete(null);
- Alert.alert("Success", "Event type deleted successfully");
- },
- onError: (deleteError) => {
- const message = deleteError instanceof Error ? deleteError.message : String(deleteError);
- console.error("Failed to delete event type", message);
- if (__DEV__) {
- const stack = deleteError instanceof Error ? deleteError.stack : undefined;
- console.debug("[EventTypes] deleteEventType failed", {
- message,
- stack,
- });
- }
- showErrorAlert("Error", "Failed to delete event type. Please try again.");
- },
- });
+ // Use native iOS Alert for confirmation
+ Alert.alert(
+ "Delete Event Type",
+ `Are you sure you want to delete "${eventType.title}"? This action cannot be undone.`,
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Delete",
+ style: "destructive",
+ onPress: () => {
+ deleteEventTypeMutation(eventType.id, {
+ onSuccess: () => {
+ Alert.alert("Success", "Event type deleted successfully");
+ },
+ onError: (deleteError) => {
+ const message =
+ deleteError instanceof Error ? deleteError.message : String(deleteError);
+ console.error("Failed to delete event type", message);
+ if (__DEV__) {
+ const stack = deleteError instanceof Error ? deleteError.stack : undefined;
+ console.debug("[EventTypes] deleteEventType failed", { message, stack });
+ }
+ showErrorAlert("Error", "Failed to delete event type. Please try again.");
+ },
+ });
+ },
+ },
+ ]
+ );
};
const handleDuplicate = (eventType: EventType) => {
@@ -500,64 +499,6 @@ export default function EventTypesIOS() {
-
- {/* Delete Confirmation Modal */}
- {
- if (!isDeleting) {
- setShowDeleteModal(false);
- setEventTypeToDelete(null);
- }
- }}>
-
-
- {/* Header with icon and title */}
-
-
- {/* Danger icon */}
-
-
-
-
- {/* Title and description */}
-
- Delete Event Type
-
- {eventTypeToDelete ? (
- <>
- This will permanently delete the "{eventTypeToDelete.title}" event type. This action
- cannot be undone.
- >
- ) : null}
-
-
-
-
-
- {/* Footer with buttons */}
-
-
- Delete
-
-
- {
- setShowDeleteModal(false);
- setEventTypeToDelete(null);
- }}
- disabled={isDeleting}>
- Cancel
-
-
-
-
-
>
);
}
diff --git a/companion/app/_layout.tsx b/companion/app/_layout.tsx
index 88c8931bef..c22b3724c0 100644
--- a/companion/app/_layout.tsx
+++ b/companion/app/_layout.tsx
@@ -21,18 +21,13 @@ function RootLayoutContent() {
headerLargeTitle: false,
title: Platform.OS === "ios" ? "You" : "Profile",
presentation:
- Platform.OS === "ios"
- ? isLiquidGlassAvailable()
- ? "formSheet"
- : "modal"
- : "containedModal",
+ Platform.OS === "ios" ? (isLiquidGlassAvailable() ? "formSheet" : "modal") : "containedModal",
// iOS-specific sheet options (ignored on Android)
sheetGrabberVisible: Platform.OS === "ios",
sheetAllowedDetents: Platform.OS === "ios" ? [0.6, 0.9] : undefined,
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
contentStyle: {
- backgroundColor:
- Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : "#FFFFFF",
+ backgroundColor: Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : "#FFFFFF",
},
headerStyle: {
backgroundColor: Platform.OS === "ios" ? "transparent" : "#FFFFFF",
@@ -40,6 +35,133 @@ function RootLayoutContent() {
headerBlurEffect: Platform.OS === "ios" && isLiquidGlassAvailable() ? undefined : "light",
}}
/>
+
+
+
+
+
+
) : (
@@ -63,8 +185,7 @@ function RootLayoutContent() {
return (
+ className={containerClass}>
{content}
diff --git a/companion/app/add-guests.ios.tsx b/companion/app/add-guests.ios.tsx
new file mode 100644
index 0000000000..d94c585db5
--- /dev/null
+++ b/companion/app/add-guests.ios.tsx
@@ -0,0 +1,125 @@
+import { osName } from "expo-device";
+import { isLiquidGlassAvailable } from "expo-glass-effect";
+import { Stack, useLocalSearchParams, useRouter } from "expo-router";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { ActivityIndicator, Alert, View } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import type { AddGuestsScreenHandle } from "@/components/screens/AddGuestsScreen";
+import AddGuestsScreenComponent from "@/components/screens/AddGuestsScreen";
+import { type Booking, CalComAPIService } from "@/services/calcom";
+
+// Semi-transparent background to prevent black flash while preserving glass effect
+const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)";
+
+function getPresentationStyle(): "formSheet" | "modal" {
+ if (isLiquidGlassAvailable() && osName !== "iPadOS") {
+ return "formSheet";
+ }
+ return "modal";
+}
+
+export default function AddGuestsIOS() {
+ const { uid } = useLocalSearchParams<{ uid: string }>();
+ const router = useRouter();
+ const insets = useSafeAreaInsets();
+ const [booking, setBooking] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSaving, setIsSaving] = useState(false);
+ const [guestCount, setGuestCount] = useState(0);
+
+ const addGuestsScreenRef = useRef(null);
+
+ useEffect(() => {
+ if (uid) {
+ setIsLoading(true);
+ CalComAPIService.getBookingByUid(uid)
+ .then(setBooking)
+ .catch(() => {
+ Alert.alert("Error", "Failed to load booking details");
+ router.back();
+ })
+ .finally(() => setIsLoading(false));
+ } else {
+ setIsLoading(false);
+ Alert.alert("Error", "Booking ID is missing");
+ router.back();
+ }
+ }, [uid, router]);
+
+ const handleSave = useCallback(() => {
+ addGuestsScreenRef.current?.submit();
+ }, []);
+
+ const handleAddGuestsSuccess = useCallback(() => {
+ router.back();
+ }, [router]);
+
+ const presentationStyle = getPresentationStyle();
+ const useGlassEffect = isLiquidGlassAvailable();
+
+ const showSaveButton = guestCount > 0;
+
+ return (
+ <>
+
+
+
+
+ router.back()}>
+
+
+
+
+ Add Guests
+
+
+ {showSaveButton ? (
+
+
+
+ ) : null}
+
+
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+ )}
+
+ >
+ );
+}
diff --git a/companion/app/(tabs)/(bookings)/add-guests.tsx b/companion/app/add-guests.tsx
similarity index 76%
rename from companion/app/(tabs)/(bookings)/add-guests.tsx
rename to companion/app/add-guests.tsx
index 6d994f89cb..df99d002e1 100644
--- a/companion/app/(tabs)/(bookings)/add-guests.tsx
+++ b/companion/app/add-guests.tsx
@@ -1,6 +1,7 @@
+import { Ionicons } from "@expo/vector-icons";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import { useCallback, useEffect, useRef, useState } from "react";
-import { ActivityIndicator, Alert, Platform, Text, View } from "react-native";
+import { ActivityIndicator, Alert, Platform, View } from "react-native";
import { AppPressable } from "@/components/AppPressable";
import type { AddGuestsScreenHandle } from "@/components/screens/AddGuestsScreen";
import AddGuestsScreenComponent from "@/components/screens/AddGuestsScreen";
@@ -40,14 +41,23 @@ export default function AddGuests() {
router.back();
}, [router]);
+ const renderHeaderLeft = useCallback(
+ () => (
+ router.back()} className="px-2 py-2">
+
+
+ ),
+ [router]
+ );
+
const renderHeaderRight = useCallback(
() => (
- Save
+
),
[handleSave, isSaving]
@@ -63,9 +73,8 @@ export default function AddGuests() {
}}
/>
- {/* iOS-only Stack.Header */}
{Platform.OS === "ios" && (
-
+
Add Guests
)}
@@ -83,17 +92,24 @@ export default function AddGuests() {
options={{
title: "Add Guests",
headerBackButtonDisplayMode: "minimal",
+ headerLeft: Platform.OS !== "ios" ? renderHeaderLeft : undefined,
headerRight: Platform.OS !== "ios" ? renderHeaderRight : undefined,
}}
/>
{Platform.OS === "ios" && (
-
+
+
+ router.back()}>
+
+
+
+
Add Guests
- Save
+
diff --git a/companion/app/availability-detail.tsx b/companion/app/availability-detail.tsx
index 88d107038a..6aa2766ca3 100644
--- a/companion/app/availability-detail.tsx
+++ b/companion/app/availability-detail.tsx
@@ -1,17 +1,47 @@
-import { Stack, useLocalSearchParams } from "expo-router";
-import { AvailabilityDetailScreen } from "@/components/screens/AvailabilityDetailScreen";
+import { Stack, useLocalSearchParams, useRouter } from "expo-router";
+import { useRef } from "react";
+import { Platform } from "react-native";
+import {
+ AvailabilityDetailScreen,
+ type AvailabilityDetailScreenHandle,
+} from "@/components/screens/AvailabilityDetailScreen";
export default function AvailabilityDetail() {
const { id } = useLocalSearchParams<{ id: string }>();
+ const router = useRouter();
+ const screenRef = useRef(null);
if (!id) {
return null;
}
+ const handleSave = () => {
+ screenRef.current?.save();
+ };
+
return (
<>
-
+
+ {Platform.OS === "ios" && (
+
+
+ router.back()}>
+
+
+
+
+ Edit Availability
+
+
+
+ Save
+
+
+
+ )}
+
+
>
);
}
diff --git a/companion/app/edit-location.ios.tsx b/companion/app/edit-location.ios.tsx
new file mode 100644
index 0000000000..e823988b32
--- /dev/null
+++ b/companion/app/edit-location.ios.tsx
@@ -0,0 +1,119 @@
+import { osName } from "expo-device";
+import { isLiquidGlassAvailable } from "expo-glass-effect";
+import { Stack, useLocalSearchParams, useRouter } from "expo-router";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { ActivityIndicator, Alert, View } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import type { EditLocationScreenHandle } from "@/components/screens/EditLocationScreen";
+import EditLocationScreenComponent from "@/components/screens/EditLocationScreen";
+import { type Booking, CalComAPIService } from "@/services/calcom";
+
+// Semi-transparent background to prevent black flash while preserving glass effect
+const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)";
+
+function getPresentationStyle(): "formSheet" | "modal" {
+ if (isLiquidGlassAvailable() && osName !== "iPadOS") {
+ return "formSheet";
+ }
+ return "modal";
+}
+
+export default function EditLocationIOS() {
+ const { uid } = useLocalSearchParams<{ uid: string }>();
+ const router = useRouter();
+ const insets = useSafeAreaInsets();
+ const [booking, setBooking] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSaving, setIsSaving] = useState(false);
+
+ const editLocationScreenRef = useRef(null);
+
+ useEffect(() => {
+ if (uid) {
+ setIsLoading(true);
+ CalComAPIService.getBookingByUid(uid)
+ .then(setBooking)
+ .catch(() => {
+ Alert.alert("Error", "Failed to load booking details");
+ router.back();
+ })
+ .finally(() => setIsLoading(false));
+ } else {
+ setIsLoading(false);
+ Alert.alert("Error", "Booking ID is missing");
+ router.back();
+ }
+ }, [uid, router]);
+
+ const handleSave = useCallback(() => {
+ editLocationScreenRef.current?.submit();
+ }, []);
+
+ const handleUpdateSuccess = useCallback(() => {
+ router.back();
+ }, [router]);
+
+ const presentationStyle = getPresentationStyle();
+ const useGlassEffect = isLiquidGlassAvailable();
+
+ return (
+ <>
+
+
+
+
+ router.back()}>
+
+
+
+
+ Edit Location
+
+
+
+
+
+
+
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+ )}
+
+ >
+ );
+}
diff --git a/companion/app/(tabs)/(bookings)/edit-location.tsx b/companion/app/edit-location.tsx
similarity index 76%
rename from companion/app/(tabs)/(bookings)/edit-location.tsx
rename to companion/app/edit-location.tsx
index 58050595ef..345de79eda 100644
--- a/companion/app/(tabs)/(bookings)/edit-location.tsx
+++ b/companion/app/edit-location.tsx
@@ -1,6 +1,7 @@
+import { Ionicons } from "@expo/vector-icons";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import { useCallback, useEffect, useRef, useState } from "react";
-import { ActivityIndicator, Alert, Platform, Text, View } from "react-native";
+import { ActivityIndicator, Alert, Platform, View } from "react-native";
import { AppPressable } from "@/components/AppPressable";
import type { EditLocationScreenHandle } from "@/components/screens/EditLocationScreen";
import EditLocationScreenComponent from "@/components/screens/EditLocationScreen";
@@ -40,14 +41,23 @@ export default function EditLocation() {
router.back();
}, [router]);
+ const renderHeaderLeft = useCallback(
+ () => (
+ router.back()} className="px-2 py-2">
+
+
+ ),
+ [router]
+ );
+
const renderHeaderRight = useCallback(
() => (
- Save
+
),
[handleSave, isSaving]
@@ -63,9 +73,8 @@ export default function EditLocation() {
}}
/>
- {/* iOS-only Stack.Header */}
{Platform.OS === "ios" && (
-
+
Edit Location
)}
@@ -83,17 +92,24 @@ export default function EditLocation() {
options={{
title: "Edit Location",
headerBackButtonDisplayMode: "minimal",
+ headerLeft: Platform.OS !== "ios" ? renderHeaderLeft : undefined,
headerRight: Platform.OS !== "ios" ? renderHeaderRight : undefined,
}}
/>
{Platform.OS === "ios" && (
-
+
+
+ router.back()}>
+
+
+
+
Edit Location
- Save
+
diff --git a/companion/app/event-type-detail.tsx b/companion/app/event-type-detail.tsx
index 5461e0097b..bbfec5e25e 100644
--- a/companion/app/event-type-detail.tsx
+++ b/companion/app/event-type-detail.tsx
@@ -15,6 +15,7 @@ import {
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { AppPressable } from "@/components/AppPressable";
import { AdvancedTab } from "@/components/event-type-detail/tabs/AdvancedTab";
import { AvailabilityTab } from "@/components/event-type-detail/tabs/AvailabilityTab";
import { BasicsTab } from "@/components/event-type-detail/tabs/BasicsTab";
@@ -117,7 +118,6 @@ const tabs: Tab[] = [
];
export default function EventTypeDetail() {
- "use no memo";
const router = useRouter();
const { id, title, description, duration, slug } = useLocalSearchParams<{
id: string;
@@ -363,18 +363,21 @@ export default function EventTypeDetail() {
const fetchScheduleDetails = useCallback(async (scheduleId: number) => {
setScheduleDetailsLoading(true);
+ let scheduleDetails: Awaited> | null = null;
try {
- const scheduleDetails = await CalComAPIService.getScheduleById(scheduleId);
- setSelectedScheduleDetails(scheduleDetails);
- if (scheduleDetails?.timeZone) {
- setSelectedTimezone(scheduleDetails.timeZone);
- }
- setScheduleDetailsLoading(false);
+ scheduleDetails = await CalComAPIService.getScheduleById(scheduleId);
} catch (error) {
safeLogError("Failed to fetch schedule details:", error);
setSelectedScheduleDetails(null);
setScheduleDetailsLoading(false);
+ return;
}
+ setSelectedScheduleDetails(scheduleDetails);
+ const timeZone = scheduleDetails?.timeZone;
+ if (timeZone) {
+ setSelectedTimezone(timeZone);
+ }
+ setScheduleDetailsLoading(false);
}, []);
const fetchSchedules = useCallback(async () => {
@@ -408,344 +411,364 @@ export default function EventTypeDetail() {
}
}, []);
+ const applyEventTypeData = useCallback((eventType: EventType) => {
+ setEventTypeData(eventType);
+
+ // Load basic fields
+ if (eventType.title) setEventTitle(eventType.title);
+ if (eventType.slug) setEventSlug(eventType.slug);
+ if (eventType.description) setEventDescription(eventType.description);
+ if (eventType.lengthInMinutes) setEventDuration(eventType.lengthInMinutes.toString());
+ if (eventType.hidden !== undefined) setIsHidden(eventType.hidden);
+
+ const eventTypeExt = eventType as EventType & EventTypeExtended;
+ const lengthOptions = eventTypeExt.lengthInMinutesOptions;
+ const hasLengthOptions =
+ lengthOptions && Array.isArray(lengthOptions) && lengthOptions.length > 0;
+ if (hasLengthOptions) {
+ setAllowMultipleDurations(true);
+ const durationStrings = lengthOptions.map((mins: number) => `${mins} mins`);
+ setSelectedDurations(durationStrings);
+ if (eventType.lengthInMinutes) {
+ setDefaultDuration(`${eventType.lengthInMinutes} mins`);
+ }
+ }
+
+ // Load buffer times
+ if (eventType.beforeEventBuffer) {
+ setBeforeEventBuffer(`${eventType.beforeEventBuffer} Minutes`);
+ }
+ if (eventType.afterEventBuffer) {
+ setAfterEventBuffer(`${eventType.afterEventBuffer} Minutes`);
+ }
+
+ // Load minimum booking notice
+ if (eventType.minimumBookingNotice) {
+ const minutes = eventType.minimumBookingNotice;
+ if (minutes >= 1440) {
+ // Days
+ setMinimumNoticeValue((minutes / 1440).toString());
+ setMinimumNoticeUnit("Days");
+ } else if (minutes >= 60) {
+ // Hours
+ setMinimumNoticeValue((minutes / 60).toString());
+ setMinimumNoticeUnit("Hours");
+ } else {
+ // Minutes
+ setMinimumNoticeValue(minutes.toString());
+ setMinimumNoticeUnit("Minutes");
+ }
+ }
+
+ // Load slot interval
+ if (eventType.slotInterval) {
+ setSlotInterval(`${eventType.slotInterval} Minutes`);
+ }
+
+ // Load booking frequency limits
+ const bookingLimitsCount = eventType.bookingLimitsCount;
+ const hasBookingLimitsCount = bookingLimitsCount && !("disabled" in bookingLimitsCount);
+ if (hasBookingLimitsCount) {
+ setLimitBookingFrequency(true);
+ const limits: { id: number; value: string; unit: string }[] = [];
+ let idCounter = 1;
+ if (bookingLimitsCount.day) {
+ limits.push({
+ id: idCounter++,
+ value: bookingLimitsCount.day.toString(),
+ unit: "Per day",
+ });
+ }
+ if (bookingLimitsCount.week) {
+ limits.push({
+ id: idCounter++,
+ value: bookingLimitsCount.week.toString(),
+ unit: "Per week",
+ });
+ }
+ if (bookingLimitsCount.month) {
+ limits.push({
+ id: idCounter++,
+ value: bookingLimitsCount.month.toString(),
+ unit: "Per month",
+ });
+ }
+ if (bookingLimitsCount.year) {
+ limits.push({
+ id: idCounter++,
+ value: bookingLimitsCount.year.toString(),
+ unit: "Per year",
+ });
+ }
+ if (limits.length > 0) {
+ setFrequencyLimits(limits);
+ }
+ }
+
+ // Load duration limits
+ const bookingLimitsDuration = eventType.bookingLimitsDuration;
+ const hasBookingLimitsDuration =
+ bookingLimitsDuration && !("disabled" in bookingLimitsDuration);
+ if (hasBookingLimitsDuration) {
+ setLimitTotalDuration(true);
+ const limits: { id: number; value: string; unit: string }[] = [];
+ let idCounter = 1;
+ if (bookingLimitsDuration.day) {
+ limits.push({
+ id: idCounter++,
+ value: bookingLimitsDuration.day.toString(),
+ unit: "Per day",
+ });
+ }
+ if (bookingLimitsDuration.week) {
+ limits.push({
+ id: idCounter++,
+ value: bookingLimitsDuration.week.toString(),
+ unit: "Per week",
+ });
+ }
+ if (bookingLimitsDuration.month) {
+ limits.push({
+ id: idCounter++,
+ value: bookingLimitsDuration.month.toString(),
+ unit: "Per month",
+ });
+ }
+ if (bookingLimitsDuration.year) {
+ limits.push({
+ id: idCounter++,
+ value: bookingLimitsDuration.year.toString(),
+ unit: "Per year",
+ });
+ }
+ if (limits.length > 0) {
+ setDurationLimits(limits);
+ }
+ }
+
+ // Load only show first slot
+ if (eventType.onlyShowFirstAvailableSlot !== undefined) {
+ setOnlyShowFirstAvailableSlot(eventType.onlyShowFirstAvailableSlot);
+ }
+
+ if (eventType.bookerActiveBookingsLimit) {
+ const bookingLimit = eventType.bookerActiveBookingsLimit as BookerActiveBookingsLimitExtended;
+ const isBookingLimitEnabled = !("disabled" in bookingLimit);
+ if (isBookingLimitEnabled) {
+ const maxBookings = bookingLimit.maximumActiveBookings ?? bookingLimit.count;
+ if (maxBookings !== undefined) {
+ setMaxActiveBookingsPerBooker(true);
+ setMaxActiveBookingsValue(maxBookings.toString());
+ }
+ if (bookingLimit.offerReschedule !== undefined) {
+ setOfferReschedule(bookingLimit.offerReschedule);
+ }
+ }
+ }
+
+ const bookingWindow = eventType.bookingWindow;
+ const hasBookingWindow = bookingWindow && !("disabled" in bookingWindow);
+ if (hasBookingWindow) {
+ setLimitFutureBookings(true);
+ if (bookingWindow.type === "range") {
+ setFutureBookingType("range");
+ const windowValue = bookingWindow.value;
+ const isValidRange = Array.isArray(windowValue) && windowValue.length === 2;
+ if (isValidRange) {
+ setRangeStartDate(windowValue[0]);
+ setRangeEndDate(windowValue[1]);
+ }
+ } else {
+ setFutureBookingType("rolling");
+ if (typeof bookingWindow.value === "number") {
+ setRollingDays(bookingWindow.value.toString());
+ }
+ setRollingCalendarDays(bookingWindow.type === "calendarDays");
+ }
+ }
+
+ const metadata = eventType.metadata;
+
+ if (eventTypeExt.disableCancelling !== undefined) {
+ setDisableCancelling(eventTypeExt.disableCancelling);
+ } else if (metadata?.disableCancelling) {
+ setDisableCancelling(true);
+ }
+
+ if (eventTypeExt.disableRescheduling !== undefined) {
+ setDisableRescheduling(eventTypeExt.disableRescheduling);
+ } else if (metadata?.disableRescheduling) {
+ setDisableRescheduling(true);
+ }
+
+ if (eventTypeExt.sendCalVideoTranscription !== undefined) {
+ setSendCalVideoTranscription(eventTypeExt.sendCalVideoTranscription);
+ } else if (metadata?.sendCalVideoTranscription) {
+ setSendCalVideoTranscription(true);
+ }
+
+ if (eventTypeExt.autoTranslate !== undefined) {
+ setAutoTranslate(eventTypeExt.autoTranslate);
+ } else if (metadata?.autoTranslate) {
+ setAutoTranslate(true);
+ }
+
+ if (metadata) {
+ const calendarEventNameValue = metadata.calendarEventName;
+ if (typeof calendarEventNameValue === "string") {
+ setCalendarEventName(calendarEventNameValue);
+ }
+ const addToCalendarEmailValue = metadata.addToCalendarEmail;
+ if (typeof addToCalendarEmailValue === "string") {
+ setAddToCalendarEmail(addToCalendarEmailValue);
+ }
+ }
+
+ // Load booker layouts
+ const bookerLayouts = eventType.bookerLayouts;
+ if (bookerLayouts) {
+ const enabledLayouts = bookerLayouts.enabledLayouts;
+ const hasEnabledLayouts = enabledLayouts && Array.isArray(enabledLayouts);
+ if (hasEnabledLayouts) {
+ setSelectedLayouts(enabledLayouts);
+ }
+ if (bookerLayouts.defaultLayout) {
+ setDefaultLayout(bookerLayouts.defaultLayout);
+ }
+ }
+
+ if (eventType.confirmationPolicy) {
+ const policy = eventType.confirmationPolicy as ConfirmationPolicyExtended;
+ const isPolicyEnabled = !("disabled" in policy) || policy.disabled === false;
+ if (isPolicyEnabled) {
+ setRequiresConfirmation(true);
+ }
+ }
+ if (eventType.requiresConfirmation !== undefined) {
+ setRequiresConfirmation(eventType.requiresConfirmation);
+ }
+
+ if (eventType.requiresBookerEmailVerification !== undefined) {
+ setRequiresBookerEmailVerification(eventType.requiresBookerEmailVerification);
+ }
+ if (eventType.hideCalendarNotes !== undefined) {
+ setHideCalendarNotes(eventType.hideCalendarNotes);
+ }
+ if (eventType.lockTimeZoneToggleOnBookingPage !== undefined) {
+ setLockTimezone(eventType.lockTimeZoneToggleOnBookingPage);
+ }
+ if (eventTypeExt.lockedTimeZone) {
+ setLockedTimezone(eventTypeExt.lockedTimeZone);
+ }
+ if (eventTypeExt.hideCalendarEventDetails !== undefined) {
+ setHideCalendarEventDetails(eventTypeExt.hideCalendarEventDetails);
+ }
+ if (eventTypeExt.hideOrganizerEmail !== undefined) {
+ setHideOrganizerEmail(eventTypeExt.hideOrganizerEmail);
+ }
+
+ // Load redirect URL
+ if (eventType.successRedirectUrl) {
+ setSuccessRedirectUrl(eventType.successRedirectUrl);
+ }
+ if (eventType.forwardParamsSuccessRedirect !== undefined) {
+ setForwardParamsSuccessRedirect(eventType.forwardParamsSuccessRedirect);
+ }
+
+ const eventTypeExtColor = eventTypeExt.color;
+ if (eventTypeExtColor) {
+ if (eventTypeExtColor.lightThemeHex) {
+ setEventTypeColorLight(eventTypeExtColor.lightThemeHex);
+ }
+ if (eventTypeExtColor.darkThemeHex) {
+ setEventTypeColorDark(eventTypeExtColor.darkThemeHex);
+ }
+ }
+ const eventTypeColor = eventType.eventTypeColor;
+ if (eventTypeColor) {
+ if (eventTypeColor.lightEventTypeColor) {
+ setEventTypeColorLight(eventTypeColor.lightEventTypeColor);
+ }
+ if (eventTypeColor.darkEventTypeColor) {
+ setEventTypeColorDark(eventTypeColor.darkEventTypeColor);
+ }
+ }
+
+ if (eventType.recurrence) {
+ const recurrence = eventType.recurrence as RecurrenceExtended;
+ const recurrenceInterval = recurrence.interval;
+ const recurrenceFrequency = recurrence.frequency;
+ const isRecurrenceEnabled =
+ recurrence.disabled !== true && recurrenceInterval && recurrenceFrequency;
+ if (isRecurrenceEnabled) {
+ setRecurringEnabled(true);
+ setRecurringInterval(recurrenceInterval.toString());
+ const freq = recurrenceFrequency as "weekly" | "monthly" | "yearly";
+ if (freq === "weekly" || freq === "monthly" || freq === "yearly") {
+ setRecurringFrequency(freq);
+ }
+ const occurrences = recurrence.occurrences;
+ setRecurringOccurrences(occurrences?.toString() || "12");
+ }
+ }
+
+ const locations = eventType.locations;
+ const hasLocations = locations && locations.length > 0;
+ if (hasLocations) {
+ const mappedLocations = locations.map((loc: ApiLocation) => mapApiLocationToItem(loc));
+ setLocations(mappedLocations);
+
+ const firstLocation = locations[0];
+ if (firstLocation.address) {
+ setLocationAddress(firstLocation.address);
+ }
+ if (firstLocation.link) {
+ setLocationLink(firstLocation.link);
+ }
+ if (firstLocation.phone) {
+ setLocationPhone(firstLocation.phone);
+ }
+ }
+
+ if (eventType.disableGuests !== undefined) {
+ setDisableGuests(eventType.disableGuests);
+ }
+
+ if (eventType.seats) {
+ const seats = eventType.seats as SeatsExtended;
+ const seatsAreEnabled =
+ seats.disabled === false || (!("disabled" in seats) && seats.seatsPerTimeSlot);
+
+ if (seatsAreEnabled) {
+ setSeatsEnabled(true);
+ if (seats.seatsPerTimeSlot) {
+ setSeatsPerTimeSlot(seats.seatsPerTimeSlot.toString());
+ }
+ if (seats.showAttendeeInfo !== undefined) {
+ setShowAttendeeInfo(seats.showAttendeeInfo);
+ }
+ if (seats.showAvailabilityCount !== undefined) {
+ setShowAvailabilityCount(seats.showAvailabilityCount);
+ }
+ }
+ }
+ }, []);
+
const fetchEventTypeData = useCallback(async () => {
if (!id) return;
+ let eventType: EventType | null = null;
try {
- const eventType = await CalComAPIService.getEventTypeById(parseInt(id, 10));
- if (eventType) {
- setEventTypeData(eventType);
-
- // Load basic fields
- if (eventType.title) setEventTitle(eventType.title);
- if (eventType.slug) setEventSlug(eventType.slug);
- if (eventType.description) setEventDescription(eventType.description);
- if (eventType.lengthInMinutes) setEventDuration(eventType.lengthInMinutes.toString());
- if (eventType.hidden !== undefined) setIsHidden(eventType.hidden);
-
- const eventTypeExt = eventType as EventType & EventTypeExtended;
- if (
- eventTypeExt.lengthInMinutesOptions &&
- Array.isArray(eventTypeExt.lengthInMinutesOptions) &&
- eventTypeExt.lengthInMinutesOptions.length > 0
- ) {
- setAllowMultipleDurations(true);
- const durationStrings = eventTypeExt.lengthInMinutesOptions.map(
- (mins: number) => `${mins} mins`
- );
- setSelectedDurations(durationStrings);
- if (eventType.lengthInMinutes) {
- setDefaultDuration(`${eventType.lengthInMinutes} mins`);
- }
- }
-
- // Load buffer times
- if (eventType.beforeEventBuffer) {
- setBeforeEventBuffer(`${eventType.beforeEventBuffer} Minutes`);
- }
- if (eventType.afterEventBuffer) {
- setAfterEventBuffer(`${eventType.afterEventBuffer} Minutes`);
- }
-
- // Load minimum booking notice
- if (eventType.minimumBookingNotice) {
- const minutes = eventType.minimumBookingNotice;
- if (minutes >= 1440) {
- // Days
- setMinimumNoticeValue((minutes / 1440).toString());
- setMinimumNoticeUnit("Days");
- } else if (minutes >= 60) {
- // Hours
- setMinimumNoticeValue((minutes / 60).toString());
- setMinimumNoticeUnit("Hours");
- } else {
- // Minutes
- setMinimumNoticeValue(minutes.toString());
- setMinimumNoticeUnit("Minutes");
- }
- }
-
- // Load slot interval
- if (eventType.slotInterval) {
- setSlotInterval(`${eventType.slotInterval} Minutes`);
- }
-
- // Load booking frequency limits
- if (eventType.bookingLimitsCount && !("disabled" in eventType.bookingLimitsCount)) {
- setLimitBookingFrequency(true);
- const limits: { id: number; value: string; unit: string }[] = [];
- let idCounter = 1;
- if (eventType.bookingLimitsCount.day) {
- limits.push({
- id: idCounter++,
- value: eventType.bookingLimitsCount.day.toString(),
- unit: "Per day",
- });
- }
- if (eventType.bookingLimitsCount.week) {
- limits.push({
- id: idCounter++,
- value: eventType.bookingLimitsCount.week.toString(),
- unit: "Per week",
- });
- }
- if (eventType.bookingLimitsCount.month) {
- limits.push({
- id: idCounter++,
- value: eventType.bookingLimitsCount.month.toString(),
- unit: "Per month",
- });
- }
- if (eventType.bookingLimitsCount.year) {
- limits.push({
- id: idCounter++,
- value: eventType.bookingLimitsCount.year.toString(),
- unit: "Per year",
- });
- }
- if (limits.length > 0) {
- setFrequencyLimits(limits);
- }
- }
-
- // Load duration limits
- if (eventType.bookingLimitsDuration && !("disabled" in eventType.bookingLimitsDuration)) {
- setLimitTotalDuration(true);
- const limits: { id: number; value: string; unit: string }[] = [];
- let idCounter = 1;
- if (eventType.bookingLimitsDuration.day) {
- limits.push({
- id: idCounter++,
- value: eventType.bookingLimitsDuration.day.toString(),
- unit: "Per day",
- });
- }
- if (eventType.bookingLimitsDuration.week) {
- limits.push({
- id: idCounter++,
- value: eventType.bookingLimitsDuration.week.toString(),
- unit: "Per week",
- });
- }
- if (eventType.bookingLimitsDuration.month) {
- limits.push({
- id: idCounter++,
- value: eventType.bookingLimitsDuration.month.toString(),
- unit: "Per month",
- });
- }
- if (eventType.bookingLimitsDuration.year) {
- limits.push({
- id: idCounter++,
- value: eventType.bookingLimitsDuration.year.toString(),
- unit: "Per year",
- });
- }
- if (limits.length > 0) {
- setDurationLimits(limits);
- }
- }
-
- // Load only show first slot
- if (eventType.onlyShowFirstAvailableSlot !== undefined) {
- setOnlyShowFirstAvailableSlot(eventType.onlyShowFirstAvailableSlot);
- }
-
- if (eventType.bookerActiveBookingsLimit) {
- const bookingLimit =
- eventType.bookerActiveBookingsLimit as BookerActiveBookingsLimitExtended;
- if (!("disabled" in bookingLimit)) {
- const maxBookings = bookingLimit.maximumActiveBookings ?? bookingLimit.count;
- if (maxBookings !== undefined) {
- setMaxActiveBookingsPerBooker(true);
- setMaxActiveBookingsValue(maxBookings.toString());
- }
- if (bookingLimit.offerReschedule !== undefined) {
- setOfferReschedule(bookingLimit.offerReschedule);
- }
- }
- }
-
- if (eventType.bookingWindow && !("disabled" in eventType.bookingWindow)) {
- setLimitFutureBookings(true);
- if (eventType.bookingWindow.type === "range") {
- setFutureBookingType("range");
- if (
- Array.isArray(eventType.bookingWindow.value) &&
- eventType.bookingWindow.value.length === 2
- ) {
- setRangeStartDate(eventType.bookingWindow.value[0]);
- setRangeEndDate(eventType.bookingWindow.value[1]);
- }
- } else {
- setFutureBookingType("rolling");
- if (typeof eventType.bookingWindow.value === "number") {
- setRollingDays(eventType.bookingWindow.value.toString());
- }
- setRollingCalendarDays(eventType.bookingWindow.type === "calendarDays");
- }
- }
-
- if (eventTypeExt.disableCancelling !== undefined) {
- setDisableCancelling(eventTypeExt.disableCancelling);
- } else if (eventType.metadata?.disableCancelling) {
- setDisableCancelling(true);
- }
-
- if (eventTypeExt.disableRescheduling !== undefined) {
- setDisableRescheduling(eventTypeExt.disableRescheduling);
- } else if (eventType.metadata?.disableRescheduling) {
- setDisableRescheduling(true);
- }
-
- if (eventTypeExt.sendCalVideoTranscription !== undefined) {
- setSendCalVideoTranscription(eventTypeExt.sendCalVideoTranscription);
- } else if (eventType.metadata?.sendCalVideoTranscription) {
- setSendCalVideoTranscription(true);
- }
-
- if (eventTypeExt.autoTranslate !== undefined) {
- setAutoTranslate(eventTypeExt.autoTranslate);
- } else if (eventType.metadata?.autoTranslate) {
- setAutoTranslate(true);
- }
-
- if (eventType.metadata) {
- const calendarEventNameValue = eventType.metadata.calendarEventName;
- if (typeof calendarEventNameValue === "string") {
- setCalendarEventName(calendarEventNameValue);
- }
- const addToCalendarEmailValue = eventType.metadata.addToCalendarEmail;
- if (typeof addToCalendarEmailValue === "string") {
- setAddToCalendarEmail(addToCalendarEmailValue);
- }
- }
-
- // Load booker layouts
- if (eventType.bookerLayouts) {
- if (
- eventType.bookerLayouts.enabledLayouts &&
- Array.isArray(eventType.bookerLayouts.enabledLayouts)
- ) {
- setSelectedLayouts(eventType.bookerLayouts.enabledLayouts);
- }
- if (eventType.bookerLayouts.defaultLayout) {
- setDefaultLayout(eventType.bookerLayouts.defaultLayout);
- }
- }
-
- if (eventType.confirmationPolicy) {
- const policy = eventType.confirmationPolicy as ConfirmationPolicyExtended;
- if (!("disabled" in policy) || policy.disabled === false) {
- setRequiresConfirmation(true);
- }
- }
- if (eventType.requiresConfirmation !== undefined) {
- setRequiresConfirmation(eventType.requiresConfirmation);
- }
-
- if (eventType.requiresBookerEmailVerification !== undefined) {
- setRequiresBookerEmailVerification(eventType.requiresBookerEmailVerification);
- }
- if (eventType.hideCalendarNotes !== undefined) {
- setHideCalendarNotes(eventType.hideCalendarNotes);
- }
- if (eventType.lockTimeZoneToggleOnBookingPage !== undefined) {
- setLockTimezone(eventType.lockTimeZoneToggleOnBookingPage);
- }
- if (eventTypeExt.lockedTimeZone) {
- setLockedTimezone(eventTypeExt.lockedTimeZone);
- }
- if (eventTypeExt.hideCalendarEventDetails !== undefined) {
- setHideCalendarEventDetails(eventTypeExt.hideCalendarEventDetails);
- }
- if (eventTypeExt.hideOrganizerEmail !== undefined) {
- setHideOrganizerEmail(eventTypeExt.hideOrganizerEmail);
- }
-
- // Load redirect URL
- if (eventType.successRedirectUrl) {
- setSuccessRedirectUrl(eventType.successRedirectUrl);
- }
- if (eventType.forwardParamsSuccessRedirect !== undefined) {
- setForwardParamsSuccessRedirect(eventType.forwardParamsSuccessRedirect);
- }
-
- if (eventTypeExt.color) {
- if (eventTypeExt.color.lightThemeHex) {
- setEventTypeColorLight(eventTypeExt.color.lightThemeHex);
- }
- if (eventTypeExt.color.darkThemeHex) {
- setEventTypeColorDark(eventTypeExt.color.darkThemeHex);
- }
- }
- if (eventType.eventTypeColor) {
- if (eventType.eventTypeColor.lightEventTypeColor) {
- setEventTypeColorLight(eventType.eventTypeColor.lightEventTypeColor);
- }
- if (eventType.eventTypeColor.darkEventTypeColor) {
- setEventTypeColorDark(eventType.eventTypeColor.darkEventTypeColor);
- }
- }
-
- if (eventType.recurrence) {
- const recurrence = eventType.recurrence as RecurrenceExtended;
- if (recurrence.disabled !== true && recurrence.interval && recurrence.frequency) {
- setRecurringEnabled(true);
- setRecurringInterval(recurrence.interval.toString());
- const freq = recurrence.frequency as "weekly" | "monthly" | "yearly";
- if (freq === "weekly" || freq === "monthly" || freq === "yearly") {
- setRecurringFrequency(freq);
- }
- setRecurringOccurrences(recurrence.occurrences?.toString() || "12");
- }
- }
-
- if (eventType.locations && eventType.locations.length > 0) {
- const mappedLocations = eventType.locations.map((loc: ApiLocation) =>
- mapApiLocationToItem(loc)
- );
- setLocations(mappedLocations);
-
- const firstLocation = eventType.locations[0];
- if (firstLocation.address) {
- setLocationAddress(firstLocation.address);
- }
- if (firstLocation.link) {
- setLocationLink(firstLocation.link);
- }
- if (firstLocation.phone) {
- setLocationPhone(firstLocation.phone);
- }
- }
-
- if (eventType.disableGuests !== undefined) {
- setDisableGuests(eventType.disableGuests);
- }
-
- if (eventType.seats) {
- const seats = eventType.seats as SeatsExtended;
- const seatsAreEnabled =
- seats.disabled === false || (!("disabled" in seats) && seats.seatsPerTimeSlot);
-
- if (seatsAreEnabled) {
- setSeatsEnabled(true);
- if (seats.seatsPerTimeSlot) {
- setSeatsPerTimeSlot(seats.seatsPerTimeSlot.toString());
- }
- if (seats.showAttendeeInfo !== undefined) {
- setShowAttendeeInfo(seats.showAttendeeInfo);
- }
- if (seats.showAvailabilityCount !== undefined) {
- setShowAvailabilityCount(seats.showAvailabilityCount);
- }
- }
- }
- }
+ eventType = await CalComAPIService.getEventTypeById(parseInt(id, 10));
} catch (error) {
safeLogError("Failed to fetch event type data:", error);
+ return;
}
- }, [id]);
+
+ if (eventType) {
+ applyEventTypeData(eventType);
+ }
+ }, [id, applyEventTypeData]);
useEffect(() => {
if (activeTab === "availability") {
@@ -775,28 +798,35 @@ export default function EventTypeDetail() {
}, []);
const formatTime = (time: string) => {
- try {
- // Handle different time formats that might come from the API
- let date: Date;
+ // Handle different time formats that might come from the API
+ // Extract conditionals outside try/catch for React Compiler
+ const isColonFormat = time.includes(":");
- if (time.includes(":")) {
- // Format like "09:00" or "09:00:00"
- const [hours, minutes] = time.split(":").map(Number);
- date = new Date();
- date.setHours(hours, minutes || 0, 0, 0);
- } else {
- // Other formats
- date = new Date(time);
- }
+ let date: Date;
- return date.toLocaleTimeString("en-US", {
- hour: "numeric",
- minute: "2-digit",
- hour12: true,
- });
- } catch {
+ if (isColonFormat) {
+ // Format like "09:00" or "09:00:00"
+ const parts = time.split(":").map(Number);
+ const hours = parts[0];
+ const minutes = parts[1] || 0;
+ date = new Date();
+ date.setHours(hours, minutes, 0, 0);
+ } else {
+ // Other formats
+ date = new Date(time);
+ }
+
+ // Check if the date is valid
+ const isValidDate = !Number.isNaN(date.getTime());
+ if (!isValidDate) {
return time; // Return original if parsing fails
}
+
+ return date.toLocaleTimeString("en-US", {
+ hour: "numeric",
+ minute: "2-digit",
+ hour12: true,
+ });
};
const getDaySchedule = () => {
@@ -847,27 +877,30 @@ export default function EventTypeDetail() {
};
const handlePreview = async () => {
+ const eventTypeSlug = eventSlug || "preview";
+ let link: string;
try {
- const eventTypeSlug = eventSlug || "preview";
- const link = await CalComAPIService.buildEventTypeLink(eventTypeSlug);
- await openInAppBrowser(link, "event type preview");
+ link = await CalComAPIService.buildEventTypeLink(eventTypeSlug);
} catch (error) {
safeLogError("Failed to generate preview link:", error);
showErrorAlert("Error", "Failed to generate preview link. Please try again.");
+ return;
}
+ await openInAppBrowser(link, "event type preview");
};
const handleCopyLink = async () => {
+ const eventTypeSlug = eventSlug || "event-link";
+ let link: string;
try {
- const eventTypeSlug = eventSlug || "event-link";
- const link = await CalComAPIService.buildEventTypeLink(eventTypeSlug);
-
- await Clipboard.setStringAsync(link);
- Alert.alert("Success", "Link copied!");
+ link = await CalComAPIService.buildEventTypeLink(eventTypeSlug);
} catch (error) {
safeLogError("Failed to copy link:", error);
showErrorAlert("Error", "Failed to copy link. Please try again.");
+ return;
}
+ await Clipboard.setStringAsync(link);
+ Alert.alert("Success", "Link copied!");
};
const handleDelete = () => {
@@ -934,199 +967,213 @@ export default function EventTypeDetail() {
// Detect create vs update mode
const isCreateMode = id === "new";
+ // Extract values with optional chaining outside try/catch for React Compiler
+ const selectedScheduleId = selectedSchedule?.id;
+
setSaving(true);
- try {
- if (isCreateMode) {
- // For CREATE mode, build full payload
- const payload: CreateEventTypePayload = {
- title: eventTitle,
- slug: eventSlug,
- lengthInMinutes: durationNum,
- };
- if (eventDescription) {
- payload.description = eventDescription;
- }
+ if (isCreateMode) {
+ // For CREATE mode, build full payload
+ const payload: CreateEventTypePayload = {
+ title: eventTitle,
+ slug: eventSlug,
+ lengthInMinutes: durationNum,
+ };
- if (locations.length > 0) {
- payload.locations = locations.map((loc) => mapItemToApiLocation(loc));
- }
-
- if (selectedSchedule) {
- payload.scheduleId = selectedSchedule.id;
- }
-
- payload.hidden = isHidden;
-
- // Create new event type
- await CalComAPIService.createEventType(payload);
- Alert.alert("Success", "Event type created successfully", [
- {
- text: "OK",
- onPress: () => router.back(),
- },
- ]);
- 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: 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,
- autoTranslate,
-
- // Seats
- seatsEnabled,
- seatsPerTimeSlot,
- showAttendeeInfo,
- showAvailabilityCount,
-
- // Recurring
- recurringEnabled,
- recurringInterval,
- recurringFrequency,
- recurringOccurrences,
- };
-
- // Build partial payload with only changed fields
- 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;
- }
-
- await CalComAPIService.updateEventType(parseInt(id, 10), payload);
- Alert.alert("Success", "Event type updated successfully");
- // Refresh event type data to sync with server
- await fetchEventTypeData();
- setSaving(false);
+ if (eventDescription) {
+ payload.description = eventDescription;
}
- } catch (error) {
- safeLogError("Failed to save event type:", error);
- const action = isCreateMode ? "create" : "update";
- showErrorAlert("Error", `Failed to ${action} event type. Please try again.`);
+
+ if (locations.length > 0) {
+ payload.locations = locations.map((loc) => mapItemToApiLocation(loc));
+ }
+
+ if (selectedScheduleId !== undefined) {
+ payload.scheduleId = selectedScheduleId;
+ }
+
+ payload.hidden = isHidden;
+
+ // Create new event type
+ try {
+ await CalComAPIService.createEventType(payload);
+ } catch (error) {
+ safeLogError("Failed to save event type:", error);
+ showErrorAlert("Error", "Failed to create event type. Please try again.");
+ setSaving(false);
+ return;
+ }
+ Alert.alert("Success", "Event type created successfully", [
+ {
+ text: "OK",
+ onPress: () => router.back(),
+ },
+ ]);
+ 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
+ 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;
+ }
+
+ try {
+ await CalComAPIService.updateEventType(parseInt(id, 10), payload);
+ } catch (error) {
+ safeLogError("Failed to save event type:", error);
+ showErrorAlert("Error", "Failed to update event type. Please try again.");
+ setSaving(false);
+ return;
+ }
+ Alert.alert("Success", "Event type updated successfully");
+ // Refresh event type data to sync with server
+ await fetchEventTypeData();
setSaving(false);
}
};
+ const headerTitle = id === "new" ? "Create Event Type" : truncateTitle(title);
+ const saveButtonText = id === "new" ? "Create" : "Save";
+
+ const renderHeaderLeft = () => (
+ router.back()} className="px-2 py-2">
+
+
+ );
+
+ const renderHeaderRight = () => (
+
+ {saveButtonText}
+
+ );
+
return (
<>
-
-
- {/* Glass Header */}
-
-
- router.back()}
- >
-
-
+
-
- {id === "new" ? "Create Event Type" : truncateTitle(title)}
-
+ {Platform.OS === "ios" && (
+
+
+ router.back()}>
+
+
+
- {headerTitle}
+
+
+
-
- {id === "new" ? "Create" : "Save"}
-
-
-
-
+ {saveButtonText}
+
+
+
+ )}
+
{/* Tabs */}
{isLiquidGlassAvailable() ? (
@@ -1164,12 +1211,7 @@ export default function EventTypeDetail() {
) : (
diff --git a/companion/app/mark-no-show.ios.tsx b/companion/app/mark-no-show.ios.tsx
new file mode 100644
index 0000000000..f20985e2be
--- /dev/null
+++ b/companion/app/mark-no-show.ios.tsx
@@ -0,0 +1,137 @@
+import { osName } from "expo-device";
+import { isLiquidGlassAvailable } from "expo-glass-effect";
+import { Stack, useLocalSearchParams, useRouter } from "expo-router";
+import { useEffect, useState } from "react";
+import { ActivityIndicator, Alert, View } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import MarkNoShowScreenComponent from "@/components/screens/MarkNoShowScreen";
+import { type Booking, CalComAPIService } from "@/services/calcom";
+
+interface Attendee {
+ id?: number | string;
+ email: string;
+ name: string;
+ noShow?: boolean;
+}
+
+interface BookingAttendee {
+ id?: number | string;
+ email: string;
+ name?: string;
+ noShow?: boolean;
+ absent?: boolean;
+}
+
+function getPresentationStyle(): "formSheet" | "modal" {
+ if (isLiquidGlassAvailable() && osName !== "iPadOS") {
+ return "formSheet";
+ }
+ return "modal";
+}
+
+export default function MarkNoShowIOS() {
+ const { uid } = useLocalSearchParams<{ uid: string }>();
+ const router = useRouter();
+ const insets = useSafeAreaInsets();
+ const [booking, setBooking] = useState(null);
+ const [attendees, setAttendees] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ if (uid) {
+ setIsLoading(true);
+ CalComAPIService.getBookingByUid(uid)
+ .then((bookingData) => {
+ setBooking(bookingData);
+ const bookingAttendees: Attendee[] = [];
+ if (bookingData.attendees && Array.isArray(bookingData.attendees)) {
+ bookingData.attendees.forEach((att: BookingAttendee) => {
+ bookingAttendees.push({
+ id: att.id,
+ email: att.email,
+ name: att.name || att.email,
+ noShow: att.absent === true || att.noShow === true,
+ });
+ });
+ }
+ setAttendees(bookingAttendees);
+ })
+ .catch(() => {
+ Alert.alert("Error", "Failed to load booking details");
+ router.back();
+ })
+ .finally(() => setIsLoading(false));
+ } else {
+ setIsLoading(false);
+ Alert.alert("Error", "Booking ID is missing");
+ router.back();
+ }
+ }, [uid, router]);
+
+ const presentationStyle = getPresentationStyle();
+ const useGlassEffect = isLiquidGlassAvailable();
+
+ return (
+ <>
+
+
+
+
+ router.back()}>
+
+
+
+
+ Mark No-Show
+
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+ {
+ setBooking(updatedBooking);
+ const updatedAttendees: Attendee[] = [];
+ if (updatedBooking.attendees && Array.isArray(updatedBooking.attendees)) {
+ updatedBooking.attendees.forEach((att: BookingAttendee) => {
+ updatedAttendees.push({
+ id: att.id,
+ email: att.email,
+ name: att.name || att.email,
+ noShow: att.absent === true || att.noShow === true,
+ });
+ });
+ }
+ setAttendees(updatedAttendees);
+ }}
+ transparentBackground={useGlassEffect}
+ />
+ )}
+
+ >
+ );
+}
diff --git a/companion/app/(tabs)/(bookings)/mark-no-show.tsx b/companion/app/mark-no-show.tsx
similarity index 82%
rename from companion/app/(tabs)/(bookings)/mark-no-show.tsx
rename to companion/app/mark-no-show.tsx
index 46b40756cf..641087fa7d 100644
--- a/companion/app/(tabs)/(bookings)/mark-no-show.tsx
+++ b/companion/app/mark-no-show.tsx
@@ -1,6 +1,8 @@
+import { Ionicons } from "@expo/vector-icons";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { ActivityIndicator, Alert, Platform, View } from "react-native";
+import { AppPressable } from "@/components/AppPressable";
import MarkNoShowScreenComponent from "@/components/screens/MarkNoShowScreen";
import { type Booking, CalComAPIService } from "@/services/calcom";
@@ -58,6 +60,15 @@ export default function MarkNoShow() {
}
}, [uid, router]);
+ const renderHeaderLeft = useCallback(
+ () => (
+ router.back()} className="px-2 py-2">
+
+
+ ),
+ [router]
+ );
+
if (isLoading) {
return (
<>
@@ -68,9 +79,8 @@ export default function MarkNoShow() {
}}
/>
- {/* iOS-only Stack.Header */}
{Platform.OS === "ios" && (
-
+
Mark No-Show
)}
@@ -88,12 +98,18 @@ export default function MarkNoShow() {
options={{
title: "Mark No-Show",
headerBackButtonDisplayMode: "minimal",
+ headerLeft: Platform.OS !== "ios" ? renderHeaderLeft : undefined,
}}
/>
- {/* iOS-only Stack.Header with native styling */}
{Platform.OS === "ios" && (
-
+
+
+ router.back()}>
+
+
+
+
Mark No-Show
)}
diff --git a/companion/app/meeting-session-details.ios.tsx b/companion/app/meeting-session-details.ios.tsx
new file mode 100644
index 0000000000..5a48ff3ef0
--- /dev/null
+++ b/companion/app/meeting-session-details.ios.tsx
@@ -0,0 +1,98 @@
+import { osName } from "expo-device";
+import { isLiquidGlassAvailable } from "expo-glass-effect";
+import { Stack, useLocalSearchParams, useRouter } from "expo-router";
+import { useEffect, useState } from "react";
+import { ActivityIndicator, Alert, View } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import MeetingSessionDetailsScreenComponent from "@/components/screens/MeetingSessionDetailsScreen";
+import { CalComAPIService } from "@/services/calcom";
+import type { ConferencingSession } from "@/services/types/bookings.types";
+
+/**
+ * Get the presentation style for the meeting session details sheet
+ * - Uses formSheet on iPhone with liquid glass support
+ * - Uses modal on iPad or older iOS devices
+ */
+function getPresentationStyle(): "formSheet" | "modal" {
+ if (isLiquidGlassAvailable() && osName !== "iPadOS") {
+ return "formSheet";
+ }
+ return "modal";
+}
+
+export default function MeetingSessionDetailsIOS() {
+ const { uid } = useLocalSearchParams<{ uid: string }>();
+ const router = useRouter();
+ const insets = useSafeAreaInsets();
+ const [sessions, setSessions] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ if (uid) {
+ setIsLoading(true);
+ CalComAPIService.getConferencingSessions(uid)
+ .then((sessionsData) => {
+ setSessions(sessionsData);
+ })
+ .catch(() => {
+ Alert.alert("Error", "Failed to load session details");
+ router.back();
+ })
+ .finally(() => setIsLoading(false));
+ } else {
+ setIsLoading(false);
+ Alert.alert("Error", "Booking ID is missing");
+ router.back();
+ }
+ }, [uid, router]);
+
+ const presentationStyle = getPresentationStyle();
+ const useGlassEffect = isLiquidGlassAvailable();
+
+ return (
+ <>
+
+
+
+
+ router.back()}>
+
+
+
+
+ Session Details
+
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+ )}
+
+ >
+ );
+}
diff --git a/companion/app/(tabs)/(bookings)/meeting-session-details.tsx b/companion/app/meeting-session-details.tsx
similarity index 73%
rename from companion/app/(tabs)/(bookings)/meeting-session-details.tsx
rename to companion/app/meeting-session-details.tsx
index 95143f0fcd..ef30fb05f8 100644
--- a/companion/app/(tabs)/(bookings)/meeting-session-details.tsx
+++ b/companion/app/meeting-session-details.tsx
@@ -1,6 +1,8 @@
+import { Ionicons } from "@expo/vector-icons";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { ActivityIndicator, Alert, Platform, View } from "react-native";
+import { AppPressable } from "@/components/AppPressable";
import MeetingSessionDetailsScreenComponent from "@/components/screens/MeetingSessionDetailsScreen";
import { CalComAPIService } from "@/services/calcom";
import type { ConferencingSession } from "@/services/types/bookings.types";
@@ -30,6 +32,15 @@ export default function MeetingSessionDetails() {
}
}, [uid, router]);
+ const renderHeaderLeft = useCallback(
+ () => (
+ router.back()} className="px-2 py-2">
+
+
+ ),
+ [router]
+ );
+
if (isLoading) {
return (
<>
@@ -40,9 +51,8 @@ export default function MeetingSessionDetails() {
}}
/>
- {/* iOS-only Stack.Header */}
{Platform.OS === "ios" && (
-
+
Session Details
)}
@@ -60,12 +70,18 @@ export default function MeetingSessionDetails() {
options={{
title: "Session Details",
headerBackButtonDisplayMode: "minimal",
+ headerLeft: Platform.OS !== "ios" ? renderHeaderLeft : undefined,
}}
/>
- {/* iOS-only Stack.Header with native styling */}
{Platform.OS === "ios" && (
-
+
+
+ router.back()}>
+
+
+
+
Session Details
)}
diff --git a/companion/app/reschedule.ios.tsx b/companion/app/reschedule.ios.tsx
new file mode 100644
index 0000000000..7d2d5bee7d
--- /dev/null
+++ b/companion/app/reschedule.ios.tsx
@@ -0,0 +1,119 @@
+import { osName } from "expo-device";
+import { isLiquidGlassAvailable } from "expo-glass-effect";
+import { Stack, useLocalSearchParams, useRouter } from "expo-router";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { ActivityIndicator, Alert, View } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import type { RescheduleScreenHandle } from "@/components/screens/RescheduleScreen";
+import RescheduleScreenComponent from "@/components/screens/RescheduleScreen";
+import { type Booking, CalComAPIService } from "@/services/calcom";
+
+// Semi-transparent background to prevent black flash while preserving glass effect
+const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)";
+
+function getPresentationStyle(): "formSheet" | "modal" {
+ if (isLiquidGlassAvailable() && osName !== "iPadOS") {
+ return "formSheet";
+ }
+ return "modal";
+}
+
+export default function RescheduleIOS() {
+ const { uid } = useLocalSearchParams<{ uid: string }>();
+ const router = useRouter();
+ const insets = useSafeAreaInsets();
+ const [booking, setBooking] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSaving, setIsSaving] = useState(false);
+
+ const rescheduleScreenRef = useRef(null);
+
+ useEffect(() => {
+ if (uid) {
+ setIsLoading(true);
+ CalComAPIService.getBookingByUid(uid)
+ .then(setBooking)
+ .catch(() => {
+ Alert.alert("Error", "Failed to load booking details");
+ router.back();
+ })
+ .finally(() => setIsLoading(false));
+ } else {
+ setIsLoading(false);
+ Alert.alert("Error", "Booking ID is missing");
+ router.back();
+ }
+ }, [uid, router]);
+
+ const handleSave = useCallback(() => {
+ rescheduleScreenRef.current?.submit();
+ }, []);
+
+ const handleRescheduleSuccess = useCallback(() => {
+ router.back();
+ }, [router]);
+
+ const presentationStyle = getPresentationStyle();
+ const useGlassEffect = isLiquidGlassAvailable();
+
+ return (
+ <>
+
+
+
+
+ router.back()}>
+
+
+
+
+ Reschedule
+
+
+
+
+
+
+
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+ )}
+
+ >
+ );
+}
diff --git a/companion/app/(tabs)/(bookings)/reschedule.tsx b/companion/app/reschedule.tsx
similarity index 76%
rename from companion/app/(tabs)/(bookings)/reschedule.tsx
rename to companion/app/reschedule.tsx
index b1228543e4..6e671bf5c7 100644
--- a/companion/app/(tabs)/(bookings)/reschedule.tsx
+++ b/companion/app/reschedule.tsx
@@ -1,6 +1,7 @@
+import { Ionicons } from "@expo/vector-icons";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import { useCallback, useEffect, useRef, useState } from "react";
-import { ActivityIndicator, Alert, Platform, Text, View } from "react-native";
+import { ActivityIndicator, Alert, Platform, View } from "react-native";
import { AppPressable } from "@/components/AppPressable";
import type { RescheduleScreenHandle } from "@/components/screens/RescheduleScreen";
import RescheduleScreenComponent from "@/components/screens/RescheduleScreen";
@@ -40,14 +41,23 @@ export default function Reschedule() {
router.back();
}, [router]);
+ const renderHeaderLeft = useCallback(
+ () => (
+ router.back()} className="px-2 py-2">
+
+
+ ),
+ [router]
+ );
+
const renderHeaderRight = useCallback(
() => (
- Save
+
),
[handleSave, isSaving]
@@ -64,7 +74,7 @@ export default function Reschedule() {
/>
{Platform.OS === "ios" && (
-
+
Reschedule
)}
@@ -82,17 +92,24 @@ export default function Reschedule() {
options={{
title: "Reschedule",
headerBackButtonDisplayMode: "minimal",
+ headerLeft: Platform.OS !== "ios" ? renderHeaderLeft : undefined,
headerRight: Platform.OS !== "ios" ? renderHeaderRight : undefined,
}}
/>
{Platform.OS === "ios" && (
-
+
+
+ router.back()}>
+
+
+
+
Reschedule
- Save
+
@@ -103,6 +120,7 @@ export default function Reschedule() {
booking={booking}
onSuccess={handleRescheduleSuccess}
onSavingChange={setIsSaving}
+ useNativeHeader
/>
>
);
diff --git a/companion/app/view-recordings.ios.tsx b/companion/app/view-recordings.ios.tsx
new file mode 100644
index 0000000000..174df23b2c
--- /dev/null
+++ b/companion/app/view-recordings.ios.tsx
@@ -0,0 +1,93 @@
+import { osName } from "expo-device";
+import { isLiquidGlassAvailable } from "expo-glass-effect";
+import { Stack, useLocalSearchParams, useRouter } from "expo-router";
+import { useEffect, useState } from "react";
+import { ActivityIndicator, Alert, View } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import ViewRecordingsScreenComponent from "@/components/screens/ViewRecordingsScreen";
+import { CalComAPIService } from "@/services/calcom";
+import type { BookingRecording } from "@/services/types/bookings.types";
+import { safeLogError } from "@/utils/safeLogger";
+
+function getPresentationStyle(): "formSheet" | "modal" {
+ if (isLiquidGlassAvailable() && osName !== "iPadOS") {
+ return "formSheet";
+ }
+ return "modal";
+}
+
+export default function ViewRecordingsIOS() {
+ const { uid } = useLocalSearchParams<{ uid: string }>();
+ const router = useRouter();
+ const insets = useSafeAreaInsets();
+ const [recordings, setRecordings] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ if (uid) {
+ setIsLoading(true);
+ CalComAPIService.getRecordings(uid)
+ .then(setRecordings)
+ .catch((error) => {
+ safeLogError("Failed to load recordings:", error);
+ Alert.alert("Error", "Failed to load recordings");
+ router.back();
+ })
+ .finally(() => setIsLoading(false));
+ } else {
+ setIsLoading(false);
+ Alert.alert("Error", "Booking ID is missing");
+ router.back();
+ }
+ }, [uid, router]);
+
+ const presentationStyle = getPresentationStyle();
+ const useGlassEffect = isLiquidGlassAvailable();
+
+ return (
+ <>
+
+
+
+
+ router.back()}>
+
+
+
+
+ Recordings
+
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+ )}
+
+ >
+ );
+}
diff --git a/companion/app/(tabs)/(bookings)/view-recordings.tsx b/companion/app/view-recordings.tsx
similarity index 73%
rename from companion/app/(tabs)/(bookings)/view-recordings.tsx
rename to companion/app/view-recordings.tsx
index db15984593..a796e98ff3 100644
--- a/companion/app/(tabs)/(bookings)/view-recordings.tsx
+++ b/companion/app/view-recordings.tsx
@@ -1,6 +1,8 @@
+import { Ionicons } from "@expo/vector-icons";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { ActivityIndicator, Alert, Platform, View } from "react-native";
+import { AppPressable } from "@/components/AppPressable";
import ViewRecordingsScreenComponent from "@/components/screens/ViewRecordingsScreen";
import { CalComAPIService } from "@/services/calcom";
import type { BookingRecording } from "@/services/types/bookings.types";
@@ -30,6 +32,15 @@ export default function ViewRecordings() {
}
}, [uid, router]);
+ const renderHeaderLeft = useCallback(
+ () => (
+ router.back()} className="px-2 py-2">
+
+
+ ),
+ [router]
+ );
+
if (isLoading) {
return (
<>
@@ -40,9 +51,8 @@ export default function ViewRecordings() {
}}
/>
- {/* iOS-only Stack.Header */}
{Platform.OS === "ios" && (
-
+
Recordings
)}
@@ -60,12 +70,18 @@ export default function ViewRecordings() {
options={{
title: "Recordings",
headerBackButtonDisplayMode: "minimal",
+ headerLeft: Platform.OS !== "ios" ? renderHeaderLeft : undefined,
}}
/>
- {/* iOS-only Stack.Header with native styling */}
{Platform.OS === "ios" && (
-
+
+
+ router.back()}>
+
+
+
+
Recordings
)}
diff --git a/companion/biome.json b/companion/biome.json
new file mode 100644
index 0000000000..3cd1189706
--- /dev/null
+++ b/companion/biome.json
@@ -0,0 +1,85 @@
+{
+ "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
+ "root": false,
+ "vcs": {
+ "enabled": true,
+ "clientKind": "git",
+ "useIgnoreFile": true
+ },
+ "files": {
+ "ignoreUnknown": true,
+ "includes": [
+ "**/*.js",
+ "**/*.jsx",
+ "**/*.ts",
+ "**/*.tsx",
+ "**/*.json",
+ "!**/node_modules",
+ "!**/.expo",
+ "!**/dist",
+ "!**/.output",
+ "!**/*.d.ts",
+ "!**/bun.lock",
+ "!**/android",
+ "!**/ios"
+ ]
+ },
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "space",
+ "indentWidth": 2,
+ "lineWidth": 100
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true,
+ "suspicious": {
+ "noExplicitAny": "error",
+ "noArrayIndexKey": "error",
+ "useIterableCallbackReturn": "error",
+ "noImplicitAnyLet": "off"
+ },
+ "correctness": {
+ "useExhaustiveDependencies": "error",
+ "useHookAtTopLevel": "error",
+ "noInvalidUseBeforeDeclaration": "off",
+ "noUnusedFunctionParameters": "error",
+ "noUnusedVariables": "error"
+ },
+ "complexity": {
+ "noStaticOnlyClass": "error"
+ },
+ "style": {
+ "noNonNullAssertion": "error"
+ },
+ "a11y": {
+ "noStaticElementInteractions": "error"
+ }
+ }
+ },
+ "javascript": {
+ "formatter": {
+ "quoteStyle": "double",
+ "trailingCommas": "es5",
+ "semicolons": "always",
+ "arrowParentheses": "always"
+ }
+ },
+ "assist": {
+ "enabled": true,
+ "actions": {
+ "source": {
+ "organizeImports": "on"
+ }
+ }
+ },
+ "overrides": [
+ {
+ "includes": ["extension/**/*.ts", "extension/**/*.tsx"],
+ "javascript": {
+ "globals": ["chrome", "browser"]
+ }
+ }
+ ]
+}
diff --git a/companion/components/LogoutConfirmModal.ios.tsx b/companion/components/LogoutConfirmModal.ios.tsx
new file mode 100644
index 0000000000..d05b204050
--- /dev/null
+++ b/companion/components/LogoutConfirmModal.ios.tsx
@@ -0,0 +1,43 @@
+import { useEffect, useRef } from "react";
+import { Alert } from "react-native";
+
+interface LogoutConfirmModalProps {
+ visible: boolean;
+ onConfirm: () => void;
+ onCancel: () => void;
+}
+
+export function LogoutConfirmModal({ visible, onConfirm, onCancel }: LogoutConfirmModalProps) {
+ // Track if alert has been shown for current visible=true state to prevent re-triggering
+ // when parent passes unstable callback references
+ const alertShownRef = useRef(false);
+
+ useEffect(() => {
+ if (visible && !alertShownRef.current) {
+ alertShownRef.current = true;
+ Alert.alert(
+ "Sign Out",
+ "Are you sure you want to sign out?",
+ [
+ {
+ text: "Cancel",
+ style: "cancel",
+ onPress: onCancel,
+ },
+ {
+ text: "Sign Out",
+ style: "destructive",
+ onPress: onConfirm,
+ },
+ ],
+ { cancelable: true, onDismiss: onCancel }
+ );
+ }
+
+ if (!visible) {
+ alertShownRef.current = false;
+ }
+ }, [visible, onConfirm, onCancel]);
+
+ return null;
+}
diff --git a/companion/components/booking-list-screen/BookingListScreen.tsx b/companion/components/booking-list-screen/BookingListScreen.tsx
index 176dd71588..0697958dfb 100644
--- a/companion/components/booking-list-screen/BookingListScreen.tsx
+++ b/companion/components/booking-list-screen/BookingListScreen.tsx
@@ -123,7 +123,7 @@ export const BookingListScreen: React.FC = ({
const handleNavigateToReschedule = React.useCallback(
(booking: Booking) => {
router.push({
- pathname: "/(tabs)/(bookings)/reschedule",
+ pathname: "/reschedule",
params: { uid: booking.uid },
});
},
@@ -134,7 +134,7 @@ export const BookingListScreen: React.FC = ({
const handleNavigateToEditLocation = React.useCallback(
(booking: Booking) => {
router.push({
- pathname: "/(tabs)/(bookings)/edit-location",
+ pathname: "/edit-location",
params: { uid: booking.uid },
});
},
@@ -145,7 +145,7 @@ export const BookingListScreen: React.FC = ({
const handleNavigateToAddGuests = React.useCallback(
(booking: Booking) => {
router.push({
- pathname: "/(tabs)/(bookings)/add-guests",
+ pathname: "/add-guests",
params: { uid: booking.uid },
});
},
@@ -156,7 +156,7 @@ export const BookingListScreen: React.FC = ({
const handleNavigateToMarkNoShow = React.useCallback(
(booking: Booking) => {
router.push({
- pathname: "/(tabs)/(bookings)/mark-no-show",
+ pathname: "/mark-no-show",
params: { uid: booking.uid },
});
},
@@ -167,7 +167,7 @@ export const BookingListScreen: React.FC = ({
const handleNavigateToViewRecordings = React.useCallback(
(booking: Booking) => {
router.push({
- pathname: "/(tabs)/(bookings)/view-recordings",
+ pathname: "/view-recordings",
params: { uid: booking.uid },
});
},
@@ -178,7 +178,7 @@ export const BookingListScreen: React.FC = ({
const handleNavigateToMeetingSessionDetails = React.useCallback(
(booking: Booking) => {
router.push({
- pathname: "/(tabs)/(bookings)/meeting-session-details",
+ pathname: "/meeting-session-details",
params: { uid: booking.uid },
});
},
@@ -194,15 +194,11 @@ export const BookingListScreen: React.FC = ({
case "upcoming":
case "unconfirmed":
// Sort by start time ascending
- return sorted.sort(
- (a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime()
- );
+ return sorted.sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
case "past":
case "cancelled":
// Sort by start time descending (latest first)
- return sorted.sort(
- (a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime()
- );
+ return sorted.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime());
default:
return sorted;
}
@@ -331,17 +327,10 @@ export const BookingListScreen: React.FC = ({
contentContainerStyle={{
padding: 20,
}}
- refreshControl={
-
- }
+ refreshControl={}
contentInsetAdjustmentBehavior="automatic"
- style={{ backgroundColor: "white" }}
- >
-
+ style={{ backgroundColor: "white" }}>
+
@@ -353,12 +342,9 @@ export const BookingListScreen: React.FC = ({
contentContainerStyle={{
padding: 20,
}}
- refreshControl={
-
- }
+ refreshControl={}
contentInsetAdjustmentBehavior="automatic"
- style={{ backgroundColor: "white" }}
- >
+ style={{ backgroundColor: "white" }}>
= ({
keyExtractor={(item) => item.key}
renderItem={renderListItem}
contentContainerStyle={{ paddingBottom: 90 }}
- refreshControl={
-
- }
+ refreshControl={}
showsVerticalScrollIndicator={false}
contentInsetAdjustmentBehavior="automatic"
style={{ backgroundColor: "white" }}
@@ -393,9 +377,7 @@ export const BookingListScreen: React.FC = ({
keyExtractor={(item) => item.key}
renderItem={renderListItem}
contentContainerStyle={{ paddingBottom: 90 }}
- refreshControl={
-
- }
+ refreshControl={}
showsVerticalScrollIndicator={false}
/>
diff --git a/companion/components/booking-modals/BookingModals.ios.tsx b/companion/components/booking-modals/BookingModals.ios.tsx
new file mode 100644
index 0000000000..0a42400b7e
--- /dev/null
+++ b/companion/components/booking-modals/BookingModals.ios.tsx
@@ -0,0 +1,234 @@
+import { Ionicons } from "@expo/vector-icons";
+import type React from "react";
+import { useEffect, useMemo, useRef } from "react";
+import { ActivityIndicator, Alert, ScrollView, Text, TouchableOpacity, View } from "react-native";
+import { BookingActionsModal } from "@/components/BookingActionsModal";
+import { FullScreenModal } from "@/components/FullScreenModal";
+import type { Booking, EventType } from "@/services/calcom";
+import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions";
+
+const EMPTY_ACTIONS: BookingActionsResult = {
+ reschedule: { visible: false, enabled: false },
+ rescheduleRequest: { visible: false, enabled: false },
+ cancel: { visible: false, enabled: false },
+ changeLocation: { visible: false, enabled: false },
+ addGuests: { visible: false, enabled: false },
+ viewRecordings: { visible: false, enabled: false },
+ meetingSessionDetails: { visible: false, enabled: false },
+ markNoShow: { visible: false, enabled: false },
+};
+
+interface BookingModalsProps {
+ showRescheduleModal: boolean;
+ rescheduleBooking: Booking | null;
+ isRescheduling: boolean;
+ onRescheduleClose: () => void;
+ onRescheduleSubmit: (date: string, time: string, reason?: string) => Promise;
+
+ showRejectModal: boolean;
+ rejectReason: string;
+ isDeclining: boolean;
+ onRejectClose: () => void;
+ onRejectSubmit: (reason?: string) => void;
+ onRejectReasonChange: (reason: string) => void;
+
+ showFilterModal?: boolean;
+ eventTypes?: EventType[];
+ eventTypesLoading?: boolean;
+ selectedEventTypeId?: number | null;
+ onFilterClose?: () => void;
+ onEventTypeSelect?: (id: number | null, label?: string | null) => void;
+
+ showBookingActionsModal: boolean;
+ selectedBooking: Booking | null;
+ onActionsClose: () => void;
+ onReschedule: () => void;
+ onCancel: () => void;
+
+ currentUserEmail?: string;
+
+ onEditLocation?: (booking: Booking) => void;
+ onAddGuests?: (booking: Booking) => void;
+ onViewRecordings?: (booking: Booking) => void;
+ onMeetingSessionDetails?: (booking: Booking) => void;
+ onMarkNoShow?: (booking: Booking) => void;
+}
+
+export const BookingModals: React.FC = ({
+ showRescheduleModal: _showRescheduleModal,
+ rescheduleBooking: _rescheduleBooking,
+ isRescheduling: _isRescheduling,
+ onRescheduleClose: _onRescheduleClose,
+ onRescheduleSubmit: _onRescheduleSubmit,
+ showRejectModal,
+ rejectReason,
+ isDeclining,
+ onRejectClose,
+ onRejectSubmit,
+
+ showFilterModal,
+ eventTypes,
+ eventTypesLoading,
+ selectedEventTypeId,
+ onFilterClose,
+ onEventTypeSelect,
+ showBookingActionsModal,
+ selectedBooking,
+ onActionsClose,
+ onReschedule,
+ onCancel,
+ currentUserEmail,
+ onEditLocation,
+ onAddGuests,
+ onViewRecordings,
+ onMeetingSessionDetails,
+ onMarkNoShow,
+}) => {
+ const actions = useMemo(() => {
+ if (!selectedBooking) return EMPTY_ACTIONS;
+ return getBookingActions({
+ booking: selectedBooking,
+ eventType: undefined,
+ currentUserId: undefined,
+ currentUserEmail: currentUserEmail,
+ isOnline: true,
+ });
+ }, [selectedBooking, currentUserEmail]);
+
+ const hasShownRejectAlert = useRef(false);
+
+ useEffect(() => {
+ if (showRejectModal && !isDeclining && !hasShownRejectAlert.current) {
+ hasShownRejectAlert.current = true;
+ Alert.prompt(
+ "Reject the booking request?",
+ "Are you sure you want to reject the booking? We'll let the person who tried to book know. You can provide a reason below.",
+ [
+ {
+ text: "Cancel",
+ style: "cancel",
+ onPress: () => {
+ hasShownRejectAlert.current = false;
+ onRejectClose();
+ },
+ },
+ {
+ text: "Reject",
+ style: "destructive",
+ onPress: (reason?: string) => {
+ hasShownRejectAlert.current = false;
+ // Pass reason directly to avoid race condition with state updates
+ onRejectSubmit(reason);
+ },
+ },
+ ],
+ "plain-text",
+ rejectReason,
+ "default"
+ );
+ } else if (!showRejectModal) {
+ hasShownRejectAlert.current = false;
+ }
+ }, [showRejectModal, isDeclining, onRejectClose, onRejectSubmit, rejectReason]);
+
+ return (
+ <>
+ {showFilterModal !== undefined && onFilterClose && onEventTypeSelect ? (
+
+
+ e.stopPropagation()}
+ className="w-[85%] max-w-[350px] rounded-2xl bg-white p-5"
+ >
+
+ Filter by Event Type
+
+
+ {eventTypesLoading ? (
+
+
+ Loading event types...
+
+ ) : (
+
+ {eventTypes?.map((eventType) => (
+ onEventTypeSelect(eventType.id, eventType.title)}
+ >
+
+ {eventType.title}
+
+ {selectedEventTypeId === eventType.id ? (
+
+ ) : null}
+
+ ))}
+
+ {eventTypes?.length === 0 ? (
+
+ No event types found
+
+ ) : null}
+
+ )}
+
+
+
+ ) : null}
+
+ {
+ if (selectedBooking && onEditLocation) {
+ onEditLocation(selectedBooking);
+ }
+ }}
+ onAddGuests={() => {
+ if (selectedBooking && onAddGuests) {
+ onAddGuests(selectedBooking);
+ }
+ }}
+ onViewRecordings={() => {
+ if (selectedBooking && onViewRecordings) {
+ onViewRecordings(selectedBooking);
+ }
+ }}
+ onMeetingSessionDetails={() => {
+ if (selectedBooking && onMeetingSessionDetails) {
+ onMeetingSessionDetails(selectedBooking);
+ }
+ }}
+ onMarkNoShow={() => {
+ if (selectedBooking && onMarkNoShow) {
+ onMarkNoShow(selectedBooking);
+ }
+ }}
+ onReportBooking={() => {
+ Alert.alert("Report Booking", "Report booking functionality is not yet available");
+ }}
+ onCancelBooking={onCancel}
+ />
+ >
+ );
+};
diff --git a/companion/components/booking-modals/BookingModals.tsx b/companion/components/booking-modals/BookingModals.tsx
index 0846be12a8..37ed7385d0 100644
--- a/companion/components/booking-modals/BookingModals.tsx
+++ b/companion/components/booking-modals/BookingModals.tsx
@@ -40,7 +40,7 @@ interface BookingModalsProps {
rejectReason: string;
isDeclining: boolean;
onRejectClose: () => void;
- onRejectSubmit: () => void;
+ onRejectSubmit: (reason?: string) => void;
onRejectReasonChange: (reason: string) => void;
// Filter modal props (optional for iOS)
@@ -150,7 +150,9 @@ export const BookingModals: React.FC = ({
onPress={() => onEventTypeSelect(eventType.id, eventType.title)}
>
{eventType.title}
@@ -272,7 +274,7 @@ export const BookingModals: React.FC = ({
{/* Reject Button */}
onRejectSubmit()}
disabled={isDeclining}
style={{ opacity: isDeclining ? 0.5 : 1 }}
>
diff --git a/companion/components/screens/AddGuestsScreen.tsx b/companion/components/screens/AddGuestsScreen.tsx
index a98f39bae6..ddfd226626 100644
--- a/companion/components/screens/AddGuestsScreen.tsx
+++ b/companion/components/screens/AddGuestsScreen.tsx
@@ -26,6 +26,8 @@ export interface AddGuestsScreenProps {
booking: Booking | null;
onSuccess: () => void;
onSavingChange?: (isSaving: boolean) => void;
+ onGuestCountChange?: (count: number) => void;
+ transparentBackground?: boolean;
}
// Handle type for parent component to call submit
@@ -39,8 +41,13 @@ function isValidEmail(email: string): boolean {
}
export const AddGuestsScreen = forwardRef(
- function AddGuestsScreen({ booking, onSuccess, onSavingChange }, ref) {
+ function AddGuestsScreen(
+ { booking, onSuccess, onSavingChange, onGuestCountChange, transparentBackground = false },
+ ref
+ ) {
const insets = useSafeAreaInsets();
+ const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
+ const pillStyle = transparentBackground ? "bg-[#E8E8ED]/50" : "bg-[#E8E8ED]";
const [email, setEmail] = useState("");
const [name, setName] = useState("");
const [guests, setGuests] = useState<{ email: string; name?: string }[]>([]);
@@ -51,6 +58,11 @@ export const AddGuestsScreen = forwardRef {
+ onGuestCountChange?.(guests.length);
+ }, [guests.length, onGuestCountChange]);
+
const handleAddGuest = useCallback(() => {
const trimmedEmail = email.trim();
if (!trimmedEmail) {
@@ -111,7 +123,7 @@ export const AddGuestsScreen = forwardRef
+
No booking data
);
@@ -120,62 +132,70 @@ export const AddGuestsScreen = forwardRef
+ className={`flex-1 ${backgroundStyle}`}>
- {/* Info note */}
-
-
-
- Guests will receive an email notification about this booking.
-
+ showsVerticalScrollIndicator={!transparentBackground}>
+ {/* Email input */}
+ {transparentBackground && (
+ Email *
+ )}
+
+ {!transparentBackground && (
+ Email *
+ )}
+
- {/* Form Card */}
-
- {/* Email input */}
-
- Email *
-
-
-
- {/* Name input */}
-
+ {/* Name input */}
+ {transparentBackground && (
+ Name (optional)
+ )}
+
+ {!transparentBackground && (
Name (optional)
-
-
+ )}
+
{/* Add button */}
+ disabled={isSaving}>
- Add Guest
+ Add
{/* Guest list */}
@@ -184,28 +204,27 @@ export const AddGuestsScreen = forwardRef
Guests to add ({guests.length})
-
+
{guests.map((guest, index) => (
-
+ index < guests.length - 1 ? "border-b border-gray-100/50" : ""
+ }`}>
+
{guest.email}
- {guest.name && (
- {guest.name}
- )}
+ {guest.name && {guest.name}}
handleRemoveGuest(index)}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
- disabled={isSaving}
- >
+ disabled={isSaving}>
diff --git a/companion/components/screens/AvailabilityDetailScreen.tsx b/companion/components/screens/AvailabilityDetailScreen.tsx
index ac8b1c916a..723e5e0f1c 100644
--- a/companion/components/screens/AvailabilityDetailScreen.tsx
+++ b/companion/components/screens/AvailabilityDetailScreen.tsx
@@ -1,7 +1,7 @@
import { Ionicons } from "@expo/vector-icons";
import { GlassView } from "expo-glass-effect";
import { useRouter } from "expo-router";
-import { useCallback, useEffect, useState } from "react";
+import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
import { ActivityIndicator, Alert, ScrollView, Switch, Text, TextInput, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { AppPressable } from "@/components/AppPressable";
@@ -13,6 +13,17 @@ import { showErrorAlert } from "@/utils/alerts";
const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
const DAY_ABBREVIATIONS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+// Map day names to numbers - module scope for React Compiler optimization
+const DAY_NAME_TO_NUMBER: Record = {
+ Sunday: 0,
+ Monday: 1,
+ Tuesday: 2,
+ Wednesday: 3,
+ Thursday: 4,
+ Friday: 5,
+ Saturday: 6,
+};
+
// Convert 24-hour time to 12-hour format with AM/PM
const formatTime12Hour = (time24: string): string => {
const [hours, minutes] = time24.split(":");
@@ -76,10 +87,17 @@ const TIME_OPTIONS = generateTimeOptions();
export interface AvailabilityDetailScreenProps {
id: string;
+ useNativeHeader?: boolean;
}
-export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps) {
- "use no memo";
+export interface AvailabilityDetailScreenHandle {
+ save: () => void;
+}
+
+export const AvailabilityDetailScreen = forwardRef<
+ AvailabilityDetailScreenHandle,
+ AvailabilityDetailScreenProps
+>(function AvailabilityDetailScreen({ id, useNativeHeader = false }, ref) {
const router = useRouter();
const insets = useSafeAreaInsets();
@@ -129,98 +147,107 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
"UTC",
];
- const fetchSchedule = useCallback(async () => {
- setLoading(true);
- try {
- const scheduleData = await CalComAPIService.getScheduleById(Number(id));
+ const processScheduleData = useCallback(
+ (scheduleData: NonNullable>>) => {
+ const name = scheduleData.name ?? "";
+ const tz = scheduleData.timeZone ?? "UTC";
+ const isDefaultSchedule = scheduleData.isDefault ?? false;
- if (scheduleData) {
- setSchedule(scheduleData);
- setScheduleName(scheduleData.name || "");
- setTimeZone(scheduleData.timeZone || "UTC");
- setIsDefault(scheduleData.isDefault || false);
+ setSchedule(scheduleData);
+ setScheduleName(name);
+ setTimeZone(tz);
+ setIsDefault(isDefaultSchedule);
- // Convert availability array to day-indexed object
- const availabilityMap: Record = {};
+ // Convert availability array to day-indexed object
+ const availabilityMap: Record = {};
- // Map day names to numbers
- const dayNameToNumber: Record = {
- Sunday: 0,
- Monday: 1,
- Tuesday: 2,
- Wednesday: 3,
- Thursday: 4,
- Friday: 5,
- Saturday: 6,
- };
-
- if (scheduleData.availability && Array.isArray(scheduleData.availability)) {
- scheduleData.availability.forEach((slot) => {
- // Handle both string day names and number day formats
- let days: number[] = [];
- if (Array.isArray(slot.days)) {
- days = slot.days
- .map((day) => {
- // If it's a day name string (e.g., "Sunday", "Monday")
- if (typeof day === "string" && dayNameToNumber[day] !== undefined) {
- return dayNameToNumber[day];
+ const availabilityArray = scheduleData.availability;
+ if (availabilityArray && Array.isArray(availabilityArray)) {
+ availabilityArray.forEach((slot) => {
+ // Handle both string day names and number day formats
+ let days: number[] = [];
+ if (Array.isArray(slot.days)) {
+ days = slot.days
+ .map((day) => {
+ // If it's a day name string (e.g., "Sunday", "Monday")
+ if (typeof day === "string" && DAY_NAME_TO_NUMBER[day] !== undefined) {
+ return DAY_NAME_TO_NUMBER[day];
+ }
+ // If it's a number string (e.g., "0", "1")
+ if (typeof day === "string") {
+ const parsed = parseInt(day, 10);
+ if (!Number.isNaN(parsed) && parsed >= 0 && parsed <= 6) {
+ return parsed;
}
- // If it's a number string (e.g., "0", "1")
- if (typeof day === "string") {
- const parsed = parseInt(day, 10);
- if (!Number.isNaN(parsed) && parsed >= 0 && parsed <= 6) {
- return parsed;
- }
- }
- // If it's already a number
- if (typeof day === "number" && day >= 0 && day <= 6) {
- return day;
- }
- return null;
- })
- .filter((day): day is number => day !== null);
+ }
+ // If it's already a number
+ if (typeof day === "number" && day >= 0 && day <= 6) {
+ return day;
+ }
+ return null;
+ })
+ .filter((day): day is number => day !== null);
+ }
+
+ days.forEach((day) => {
+ if (!availabilityMap[day]) {
+ availabilityMap[day] = [];
}
-
- days.forEach((day) => {
- if (!availabilityMap[day]) {
- availabilityMap[day] = [];
- }
- availabilityMap[day].push({
- days: [day.toString()],
- startTime: slot.startTime || "09:00:00",
- endTime: slot.endTime || "17:00:00",
- });
+ const startTime = slot.startTime ?? "09:00:00";
+ const endTime = slot.endTime ?? "17:00:00";
+ availabilityMap[day].push({
+ days: [day.toString()],
+ startTime,
+ endTime,
});
});
- }
-
- setAvailability(availabilityMap);
-
- // Load overrides if they exist
- if (scheduleData.overrides && Array.isArray(scheduleData.overrides)) {
- const formattedOverrides = scheduleData.overrides.map((override) => ({
- date: override.date || "",
- startTime: override.startTime || "00:00",
- endTime: override.endTime || "00:00",
- }));
- setOverrides(formattedOverrides);
- } else {
- setOverrides([]);
- }
+ });
}
- setLoading(false);
+
+ setAvailability(availabilityMap);
+
+ // Load overrides if they exist
+ const overridesArray = scheduleData.overrides;
+ if (overridesArray && Array.isArray(overridesArray)) {
+ const formattedOverrides = overridesArray.map((override) => {
+ const date = override.date ?? "";
+ const startTime = override.startTime ?? "00:00";
+ const endTime = override.endTime ?? "00:00";
+ return { date, startTime, endTime };
+ });
+ setOverrides(formattedOverrides);
+ } else {
+ setOverrides([]);
+ }
+ },
+ []
+ );
+
+ const fetchSchedule = useCallback(async () => {
+ setLoading(true);
+ let scheduleData: Awaited> = null;
+ try {
+ scheduleData = await CalComAPIService.getScheduleById(Number(id));
} catch (error) {
// Avoid logging raw error objects from API calls (may contain sensitive response data).
console.error("Error fetching schedule");
if (__DEV__) {
const message = error instanceof Error ? error.message : String(error);
- console.debug("[AvailabilityDetailScreen] fetchSchedule failed", { message });
+ console.debug("[AvailabilityDetailScreen] fetchSchedule failed", {
+ message,
+ });
}
showErrorAlert("Error", "Failed to load schedule. Please try again.");
router.back();
setLoading(false);
+ return;
}
- }, [id, router]);
+
+ if (scheduleData) {
+ processScheduleData(scheduleData);
+ }
+ setLoading(false);
+ }, [id, router, processScheduleData]);
useEffect(() => {
if (id) {
@@ -354,6 +381,11 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
}
};
+ // Expose save method to parent via ref
+ useImperativeHandle(ref, () => ({
+ save: handleSave,
+ }));
+
const handleSetAsDefault = async () => {
try {
await CalComAPIService.updateSchedule(Number(id), {
@@ -464,52 +496,54 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
return (
- {/* Glass Header */}
-
-
- router.back()}
- >
-
-
+ {/* Glass Header - only show when not using native header */}
+ {!useNativeHeader && (
+
+
+ router.back()}
+ >
+
+
-
- Edit Availability
-
+
+ Edit Availability
+
-
- Save
-
-
-
+
+ Save
+
+
+
+ )}
@@ -551,7 +585,9 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
onValueChange={() => toggleDay(dayIndex)}
trackColor={{ false: "#E5E5EA", true: "#34C759" }}
thumbColor="#fff"
- style={{ transform: [{ scaleX: 0.8 }, { scaleY: 0.8 }] }}
+ style={{
+ transform: [{ scaleX: 0.8 }, { scaleY: 0.8 }],
+ }}
/>
- setShowTimePicker({ dayIndex, slotIndex: 0, type: "start" })
+ setShowTimePicker({
+ dayIndex,
+ slotIndex: 0,
+ type: "start",
+ })
}
className="rounded-lg border border-[#E5E5EA] bg-white px-2 py-1"
style={{ width: 85 }}
@@ -575,7 +615,11 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
-
- setShowTimePicker({ dayIndex, slotIndex: 0, type: "end" })
+ setShowTimePicker({
+ dayIndex,
+ slotIndex: 0,
+ type: "end",
+ })
}
className="rounded-lg border border-[#E5E5EA] bg-white px-2 py-1"
style={{ width: 85 }}
@@ -1017,4 +1061,4 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
);
-}
+});
diff --git a/companion/components/screens/BookingDetailScreen.ios.tsx b/companion/components/screens/BookingDetailScreen.ios.tsx
index 966f1cd2cd..9c140f6d0e 100644
--- a/companion/components/screens/BookingDetailScreen.ios.tsx
+++ b/companion/components/screens/BookingDetailScreen.ios.tsx
@@ -76,10 +76,7 @@ export interface BookingDetailScreenProps {
}) => void;
}
-export function BookingDetailScreen({
- uid,
- onActionsReady,
-}: BookingDetailScreenProps): React.JSX.Element {
+export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreenProps): React.JSX.Element {
const router = useRouter();
const insets = useSafeAreaInsets();
@@ -108,7 +105,9 @@ export function BookingDetailScreen({
console.error("Failed to cancel booking");
if (__DEV__) {
const message = err instanceof Error ? err.message : String(err);
- console.debug("[BookingDetailScreen.ios] cancelBooking failed", { message });
+ console.debug("[BookingDetailScreen.ios] cancelBooking failed", {
+ message,
+ });
}
showErrorAlert("Error", "Failed to cancel booking. Please try again.");
setIsCancelling(false);
@@ -151,7 +150,7 @@ export function BookingDetailScreen({
const openRescheduleModal = useCallback(() => {
if (!booking) return;
router.push({
- pathname: "/(tabs)/(bookings)/reschedule",
+ pathname: "/reschedule",
params: { uid: booking.uid },
});
}, [booking, router]);
@@ -159,7 +158,7 @@ export function BookingDetailScreen({
const openEditLocationModal = useCallback(() => {
if (!booking) return;
router.push({
- pathname: "/(tabs)/(bookings)/edit-location",
+ pathname: "/edit-location",
params: { uid: booking.uid },
});
}, [booking, router]);
@@ -167,7 +166,7 @@ export function BookingDetailScreen({
const openAddGuestsModal = useCallback(() => {
if (!booking) return;
router.push({
- pathname: "/(tabs)/(bookings)/add-guests",
+ pathname: "/add-guests",
params: { uid: booking.uid },
});
}, [booking, router]);
@@ -175,7 +174,7 @@ export function BookingDetailScreen({
const openMarkNoShowModal = useCallback(() => {
if (!booking) return;
router.push({
- pathname: "/(tabs)/(bookings)/mark-no-show",
+ pathname: "/mark-no-show",
params: { uid: booking.uid },
});
}, [booking, router]);
@@ -183,7 +182,7 @@ export function BookingDetailScreen({
const openViewRecordingsModal = useCallback(() => {
if (!booking) return;
router.push({
- pathname: "/(tabs)/(bookings)/view-recordings",
+ pathname: "/view-recordings",
params: { uid: booking.uid },
});
}, [booking, router]);
@@ -191,7 +190,7 @@ export function BookingDetailScreen({
const openMeetingSessionDetailsModal = useCallback(() => {
if (!booking) return;
router.push({
- pathname: "/(tabs)/(bookings)/meeting-session-details",
+ pathname: "/meeting-session-details",
params: { uid: booking.uid },
});
}, [booking, router]);
@@ -323,15 +322,27 @@ export function BookingDetailScreen({
const isNoShow = attendee.noShow || attendee.absent;
if (isPastBooking && isNoShow) {
- return { name: "close-circle" as const, color: "#FF3B30", label: "No-show" };
+ return {
+ name: "close-circle" as const,
+ color: "#FF3B30",
+ label: "No-show",
+ };
}
if (normalizedStatus === "pending") {
- return { name: "help-circle" as const, color: "#8E8E93", label: "Pending" };
+ return {
+ name: "help-circle" as const,
+ color: "#8E8E93",
+ label: "Pending",
+ };
}
if (normalizedStatus === "cancelled" || normalizedStatus === "rejected") {
- return { name: "close-circle-outline" as const, color: "#8E8E93", label: null };
+ return {
+ name: "close-circle-outline" as const,
+ color: "#8E8E93",
+ label: null,
+ };
}
return { name: "checkmark-circle" as const, color: "#34C759", label: null };
@@ -346,15 +357,13 @@ export function BookingDetailScreen({
paddingTop: 16,
paddingBottom: insets.bottom + 100,
}}
- showsVerticalScrollIndicator={false}
- >
+ showsVerticalScrollIndicator={false}>
{/* Title Section - iOS Calendar Style */}
{/* Meeting Title */}
+ style={{ letterSpacing: -0.3 }}>
{booking.title}
@@ -367,9 +376,7 @@ export function BookingDetailScreen({
)}
{durationFormatted && (
-
- {durationFormatted}
-
+ {durationFormatted}
)}
@@ -389,8 +396,7 @@ export function BookingDetailScreen({
setParticipantsExpanded(!participantsExpanded)}
- >
+ onPress={() => setParticipantsExpanded(!participantsExpanded)}>
Participants
{totalParticipants}
@@ -410,8 +416,9 @@ export function BookingDetailScreen({
? booking.hosts.map((host, index) => (
0 ? "border-t border-[#E5E5EA]" : ""}`}
- >
+ className={`flex-row items-center py-2 ${
+ index > 0 ? "border-t border-[#E5E5EA]" : ""
+ }`}>
{host.name || host.email || "Host"}
@@ -436,11 +443,8 @@ export function BookingDetailScreen({
0 || booking.hosts?.length || booking.user
- ? "border-t border-[#E5E5EA]"
- : ""
- }`}
- >
+ index > 0 || booking.hosts?.length || booking.user ? "border-t border-[#E5E5EA]" : ""
+ }`}>
{attendee.name || attendee.email}
@@ -462,8 +466,7 @@ export function BookingDetailScreen({
index > 0 || booking.attendees?.length || booking.hosts?.length || booking.user
? "border-t border-[#E5E5EA]"
: ""
- }`}
- >
+ }`}>
{guestEmail}
@@ -491,23 +494,21 @@ export function BookingDetailScreen({
"additionalNotes",
];
- const displayableEntries = Object.entries(booking.bookingFieldsResponses).filter(
- ([key, value]) => {
- if (excludedKeys.includes(key)) return false;
+ const displayableEntries = Object.entries(booking.bookingFieldsResponses).filter(([key, value]) => {
+ if (excludedKeys.includes(key)) return false;
- if (value === null || value === undefined || value === "") return false;
+ if (value === null || value === undefined || value === "") return false;
- if (Array.isArray(value) && value.length === 0) return false;
+ if (Array.isArray(value) && value.length === 0) return false;
- if (typeof value === "object" && !Array.isArray(value) && value !== null) {
- const obj = value as Record;
- if ("value" in obj && "optionValue" in obj) return false;
- if (Object.keys(obj).length === 0) return false;
- }
-
- return true;
+ if (typeof value === "object" && !Array.isArray(value) && value !== null) {
+ const obj = value as Record;
+ if ("value" in obj && "optionValue" in obj) return false;
+ if (Object.keys(obj).length === 0) return false;
}
- );
+
+ return true;
+ });
if (displayableEntries.length === 0) return null;
@@ -537,10 +538,7 @@ export function BookingDetailScreen({
.trim();
return (
- 0 ? "border-t border-[#E5E5EA]" : ""}`}
- >
+ 0 ? "border-t border-[#E5E5EA]" : ""}`}>
{displayKey}
{displayValue}
@@ -585,8 +583,7 @@ export function BookingDetailScreen({
params: { uid: fromUid },
});
}
- }}
- >
+ }}>
Rescheduled from
View original
@@ -599,12 +596,14 @@ export function BookingDetailScreen({
{
- router.push({
- pathname: "/(tabs)/(bookings)/booking-detail",
- params: { uid: booking.rescheduledToUid },
- });
- }}
- >
+ const uid = booking.rescheduledToUid;
+ if (uid) {
+ router.push({
+ pathname: "/(tabs)/(bookings)/booking-detail",
+ params: { uid },
+ });
+ }
+ }}>
Rescheduled to
View new
@@ -615,8 +614,11 @@ export function BookingDetailScreen({
{booking.reschedulingReason && (
+ className={`py-2 ${
+ booking.rescheduledFromUid || booking.rescheduledToUid || booking.fromReschedule
+ ? "border-t border-[#E5E5EA]"
+ : ""
+ }`}>
Reason
{booking.reschedulingReason}
@@ -642,9 +644,7 @@ export function BookingDetailScreen({
)}
{booking.cancelledByEmail && (
-
+
Cancelled by
{booking.cancelledByEmail}
@@ -652,8 +652,11 @@ export function BookingDetailScreen({
{booking.absentHost && (
+ className={`flex-row items-center py-2 ${
+ booking.cancellationReason || booking.cancelledByEmail
+ ? "border-t border-[#E5E5EA]"
+ : ""
+ }`}>
Host was absent
diff --git a/companion/components/screens/BookingDetailScreen.tsx b/companion/components/screens/BookingDetailScreen.tsx
index eb213d9175..998af166f4 100644
--- a/companion/components/screens/BookingDetailScreen.tsx
+++ b/companion/components/screens/BookingDetailScreen.tsx
@@ -268,7 +268,7 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
const openRescheduleModal = useCallback(() => {
if (!booking) return;
router.push({
- pathname: "/(tabs)/(bookings)/reschedule",
+ pathname: "/reschedule",
params: { uid: booking.uid },
});
}, [booking, router]);
@@ -277,7 +277,7 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
const openEditLocationModal = useCallback(() => {
if (!booking) return;
router.push({
- pathname: "/(tabs)/(bookings)/edit-location",
+ pathname: "/edit-location",
params: { uid: booking.uid },
});
}, [booking, router]);
@@ -286,7 +286,7 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
const openAddGuestsModal = useCallback(() => {
if (!booking) return;
router.push({
- pathname: "/(tabs)/(bookings)/add-guests",
+ pathname: "/add-guests",
params: { uid: booking.uid },
});
}, [booking, router]);
@@ -295,7 +295,7 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
const openMarkNoShowModal = useCallback(() => {
if (!booking) return;
router.push({
- pathname: "/(tabs)/(bookings)/mark-no-show",
+ pathname: "/mark-no-show",
params: { uid: booking.uid },
});
}, [booking, router]);
@@ -304,7 +304,7 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
const openViewRecordingsModal = useCallback(() => {
if (!booking) return;
router.push({
- pathname: "/(tabs)/(bookings)/view-recordings",
+ pathname: "/view-recordings",
params: { uid: booking.uid },
});
}, [booking, router]);
@@ -313,7 +313,7 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
const openMeetingSessionDetailsModal = useCallback(() => {
if (!booking) return;
router.push({
- pathname: "/(tabs)/(bookings)/meeting-session-details",
+ pathname: "/meeting-session-details",
params: { uid: booking.uid },
});
}, [booking, router]);
@@ -446,8 +446,7 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
headerRight: () => (
setShowActionsModal(true)}
- >
+ onPress={() => setShowActionsModal(true)}>
),
@@ -479,9 +478,7 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
-
- {booking.user.name}
-
+ {booking.user.name}
host
@@ -493,8 +490,7 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
booking.hosts.map((host, hostIndex) => (
0 ? "mt-4" : ""}`}
- >
+ className={`flex-row items-start ${hostIndex > 0 ? "mt-4" : ""}`}>
{getInitials(host.name || "Host")}
@@ -502,9 +498,7 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
-
- {host.name || "Host"}
-
+ {host.name || "Host"}
host
@@ -523,15 +517,11 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
(attendee as { noShow?: boolean; absent?: boolean }).noShow === true ||
(attendee as { noShow?: boolean; absent?: boolean }).absent === true;
return (
- 0 ? "mt-4" : ""}`}
- >
+ 0 ? "mt-4" : ""}`}>
+ }`}>
{getInitials(attendee.name)}
@@ -541,16 +531,13 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
+ }`}>
{attendee.name}
{isNoShow && (
-
- No-show
-
+ No-show
)}
@@ -570,10 +557,7 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
Where
{locationProvider.url ? (
-
+
{locationProvider.iconUrl ? (
Recurring Event
Every 2 weeks for 6 occurrences
@@ -624,19 +607,11 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
{locationProvider?.url ? (
+ className="mb-2 flex-row items-center justify-center rounded-lg bg-black px-6 py-4">
{locationProvider.iconUrl ? (
-
+
) : null}
-
- Join {locationProvider.label}
-
+ Join {locationProvider.label}
) : null}
@@ -664,9 +639,7 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
-
- Cancelling booking...
-
+ Cancelling booking...
)}
diff --git a/companion/components/screens/EditLocationScreen.ios.tsx b/companion/components/screens/EditLocationScreen.ios.tsx
index 2541553cd9..6a18561917 100644
--- a/companion/components/screens/EditLocationScreen.ios.tsx
+++ b/companion/components/screens/EditLocationScreen.ios.tsx
@@ -56,6 +56,7 @@ export interface EditLocationScreenProps {
booking: Booking | null;
onSuccess: () => void;
onSavingChange?: (isSaving: boolean) => void;
+ transparentBackground?: boolean;
}
// Handle type for parent component to call submit
@@ -68,11 +69,7 @@ const detectLocationType = (location: string): LocationTypeId => {
if (!location) return "link";
// Check if it's a URL
- if (
- location.startsWith("http://") ||
- location.startsWith("https://") ||
- location.startsWith("www.")
- ) {
+ if (location.startsWith("http://") || location.startsWith("https://") || location.startsWith("www.")) {
return "link";
}
@@ -87,18 +84,19 @@ const detectLocationType = (location: string): LocationTypeId => {
};
export const EditLocationScreen = forwardRef(
- function EditLocationScreen({ booking, onSuccess, onSavingChange }, ref) {
+ function EditLocationScreen({ booking, onSuccess, onSavingChange, transparentBackground = false }, ref) {
const insets = useSafeAreaInsets();
+ const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
const [selectedType, setSelectedType] = useState("link");
const [inputValue, setInputValue] = useState("");
const [isSaving, setIsSaving] = useState(false);
- // Pre-fill with current location
+ // Detect location type from current location but don't pre-fill the input
useEffect(() => {
if (booking?.location) {
const detectedType = detectLocationType(booking.location);
setSelectedType(detectedType);
- setInputValue(booking.location);
+ // Don't pre-fill - keep input blank so user can enter new location
}
}, [booking?.location]);
@@ -157,9 +155,7 @@ export const EditLocationScreen = forwardRef
+
No booking data
);
}
return (
-
+
- {/* Current Location Info */}
- {booking.location && (
-
-
-
-
-
- Current Location
-
- {booking.location}
-
-
-
- )}
+ showsVerticalScrollIndicator={!transparentBackground}>
+ {transparentBackground ? (
+ <>
+ {/* Location Type Selector with Native Context Menu - Glass UI */}
+
+
+
+
+
+ {selectedTypeConfig.label}
+ {selectedTypeConfig.description}
+
- {/* Location Type Selector with Native Context Menu */}
-
- Location Type
-
-
-
-
-
-
-
+ {/* Native iOS Context Menu Button */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Location Input - Glass UI */}
+
+
+
+ >
+ ) : (
+ <>
+ {/* Current Location Info */}
+ {booking.location && (
+
+
+
+
+
+ Current Location
+
+ {booking.location}
+
+
+
+ )}
+
+ {/* Location Type Selector with Native Context Menu */}
+
+ Location Type
+
+
+
+
+
+
+ {selectedTypeConfig.label}
+ {selectedTypeConfig.description}
+
+
+ {/* Native iOS Context Menu Button */}
+
+
+
+ handleTypeSelect("link")}
+ label={LOCATION_TYPES.link.label}
+ />
+ handleTypeSelect("phone")}
+ label={LOCATION_TYPES.phone.label}
+ />
+ handleTypeSelect("address")}
+ label={LOCATION_TYPES.address.label}
+ />
+
+
+
+
+
+
+
+
+
+
+ {/* Location Input */}
+
{selectedTypeConfig.label}
-
- {selectedTypeConfig.description}
-
-
-
- {/* Native iOS Context Menu Button */}
-
-
-
- handleTypeSelect("link")}
- label={LOCATION_TYPES.link.label}
- />
- handleTypeSelect("phone")}
- label={LOCATION_TYPES.phone.label}
- />
- handleTypeSelect("address")}
- label={LOCATION_TYPES.address.label}
- />
-
-
-
-
-
-
-
-
-
-
- {/* Location Input */}
-
- {selectedTypeConfig.label}
-
-
-
-
-
- {/* Info note */}
-
-
-
- Updating the location will notify attendees. Note: Calendar events may not be updated
- automatically.
-
-
+
+
+
+ >
+ )}
);
diff --git a/companion/components/screens/EditLocationScreen.tsx b/companion/components/screens/EditLocationScreen.tsx
index 145ef614d5..8cf8413840 100644
--- a/companion/components/screens/EditLocationScreen.tsx
+++ b/companion/components/screens/EditLocationScreen.tsx
@@ -76,6 +76,7 @@ export interface EditLocationScreenProps {
onSavingChange?: (isSaving: boolean) => void;
selectedType?: LocationTypeId;
onTypeChange?: (type: LocationTypeId) => void;
+ transparentBackground?: boolean;
}
export interface EditLocationScreenHandle {
@@ -85,11 +86,7 @@ export interface EditLocationScreenHandle {
const detectLocationType = (location: string): LocationTypeId => {
if (!location) return "link";
- if (
- location.startsWith("http://") ||
- location.startsWith("https://") ||
- location.startsWith("www.")
- ) {
+ if (location.startsWith("http://") || location.startsWith("https://") || location.startsWith("www.")) {
return "link";
}
@@ -102,18 +99,21 @@ const detectLocationType = (location: string): LocationTypeId => {
};
export const EditLocationScreen = forwardRef(
- function EditLocationScreen({ booking, onSuccess, onSavingChange }, ref) {
+ function EditLocationScreen({ booking, onSuccess, onSavingChange, transparentBackground = false }, ref) {
const insets = useSafeAreaInsets();
+ const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
+ const pillStyle = transparentBackground ? "bg-[#E8E8ED]/50" : "bg-[#E8E8ED]";
const [selectedType, setSelectedType] = useState("link");
const [inputValue, setInputValue] = useState("");
const [showTypePicker, setShowTypePicker] = useState(false);
const [isSaving, setIsSaving] = useState(false);
+ // Detect location type from current location but don't pre-fill the input
useEffect(() => {
if (booking?.location) {
const detectedType = detectLocationType(booking.location);
setSelectedType(detectedType);
- setInputValue(booking.location);
+ // Don't pre-fill - keep input blank so user can enter new location
}
}, [booking?.location]);
@@ -160,9 +160,7 @@ export const EditLocationScreen = forwardRef
+
No booking data
);
@@ -191,17 +189,19 @@ export const EditLocationScreen = forwardRef
+ className={`flex-1 ${backgroundStyle}`}>
- {/* Current Location Info */}
- {booking.location && (
+ showsVerticalScrollIndicator={!transparentBackground}>
+ {/* Current Location Info - hidden in transparent mode */}
+ {!transparentBackground && booking.location && (
-
+
@@ -214,34 +214,45 @@ export const EditLocationScreen = forwardRef
- Location Type
-
+ {!transparentBackground && (
+
+ Location Type
+
+ )}
setShowTypePicker(true)}
disabled={isSaving}
- activeOpacity={0.7}
- >
-
+ activeOpacity={0.7}>
+
-
- {selectedTypeConfig.label}
-
-
- {selectedTypeConfig.description}
-
+ {selectedTypeConfig.label}
+ {selectedTypeConfig.description}
{/* Location Input */}
-
- {selectedTypeConfig.label}
-
-
+ {!transparentBackground && (
+
+ {selectedTypeConfig.label}
+
+ )}
+
-
- {/* Info note */}
-
-
-
- Updating the location will notify attendees. Note: Calendar events may not be
- updated automatically.
-
-
@@ -275,17 +277,14 @@ export const EditLocationScreen = forwardRef setShowTypePicker(false)}
- >
+ onRequestClose={() => setShowTypePicker(false)}>
setShowTypePicker(false)}
- >
+ onPress={() => setShowTypePicker(false)}>
+ className="max-h-[70%] w-[320px] overflow-hidden rounded-2xl bg-white">
Select Location Type
@@ -298,34 +297,25 @@ export const EditLocationScreen = forwardRef {
setSelectedType(item.id);
if (selectedType !== item.id) {
setInputValue("");
}
setShowTypePicker(false);
- }}
- >
+ }}>
-
+ }`}>
+
+ }`}>
{item.label}
{item.description}
@@ -337,8 +327,7 @@ export const EditLocationScreen = forwardRef
setShowTypePicker(false)}
- >
+ onPress={() => setShowTypePicker(false)}>
Cancel
diff --git a/companion/components/screens/MarkNoShowScreen.tsx b/companion/components/screens/MarkNoShowScreen.tsx
index 4970d40238..e19da0ab43 100644
--- a/companion/components/screens/MarkNoShowScreen.tsx
+++ b/companion/components/screens/MarkNoShowScreen.tsx
@@ -24,6 +24,7 @@ export interface MarkNoShowScreenProps {
attendees: Attendee[];
onUpdate: (attendees: Attendee[]) => void;
onBookingUpdate?: (booking: Booking) => void;
+ transparentBackground?: boolean;
}
const getInitials = (name: string): string => {
@@ -50,9 +51,12 @@ export function MarkNoShowScreen({
attendees,
onUpdate,
onBookingUpdate,
+ transparentBackground = false,
}: MarkNoShowScreenProps) {
"use no memo";
const insets = useSafeAreaInsets();
+ const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
+ const pillStyle = transparentBackground ? "bg-[#E8E8ED]/50" : "bg-[#E8E8ED]";
const safeAttendees = Array.isArray(attendees) ? attendees : [];
const [processingEmail, setProcessingEmail] = useState(null);
@@ -140,35 +144,35 @@ export function MarkNoShowScreen({
const isProcessing = processingEmail === item.email;
const isLast = index === safeAttendees.length - 1;
+ // For transparent mode (iOS glass UI), use consistent styling regardless of no-show state
+ const itemBgStyle = transparentBackground ? "bg-white/60" : isNoShow ? "bg-[#FEF2F2]" : "bg-white";
+
return (
handleMarkNoShow(item)}
disabled={isProcessing}
- activeOpacity={0.7}
- >
+ activeOpacity={0.7}>
+ transparentBackground ? "bg-[#000]" : isNoShow ? "bg-[#FECACA]" : pillStyle
+ }`}>
+ className={`text-[16px] font-semibold ${
+ transparentBackground ? "text-white" : isNoShow ? "text-[#DC2626]" : "text-gray-600"
+ }`}>
{getInitials(item.name)}
-
- {item.name || "Unknown"}
-
-
- {item.email}
-
+ {item.name || "Unknown"}
+ {item.email}
{isNoShow && (
@@ -182,22 +186,15 @@ export function MarkNoShowScreen({
+ isNoShow ? "border border-[#16A34A] bg-white" : "border border-[#FEE2E2] bg-[#FEE2E2]"
+ }`}>
-
+
{isNoShow ? "Unmark" : "Mark"}
@@ -209,23 +206,15 @@ export function MarkNoShowScreen({
if (!booking) {
return (
-
+
No booking data
);
}
return (
-
+
- {/* Info note */}
-
-
-
- Tap an attendee to mark them as no-show or to undo a previous no-show marking.
-
-
-
{safeAttendees.length === 0 ? (
@@ -241,15 +230,25 @@ export function MarkNoShowScreen({
Attendees ({safeAttendees.length})
-
+ {transparentBackground ? (
item.email}
showsVerticalScrollIndicator={false}
- contentContainerStyle={{ paddingBottom: insets.bottom + 16 }}
+ scrollEnabled={false}
/>
-
+ ) : (
+
+ item.email}
+ showsVerticalScrollIndicator={false}
+ contentContainerStyle={{ paddingBottom: insets.bottom + 16 }}
+ />
+
+ )}
>
)}
diff --git a/companion/components/screens/MeetingSessionDetailsScreen.tsx b/companion/components/screens/MeetingSessionDetailsScreen.tsx
index a9bc3b04b2..008950ad9b 100644
--- a/companion/components/screens/MeetingSessionDetailsScreen.tsx
+++ b/companion/components/screens/MeetingSessionDetailsScreen.tsx
@@ -12,6 +12,7 @@ import type { ConferencingSession } from "@/services/types/bookings.types";
export interface MeetingSessionDetailsScreenProps {
sessions: ConferencingSession[];
+ transparentBackground?: boolean;
}
function formatDuration(seconds?: number): string {
@@ -44,15 +45,26 @@ function formatTime(dateString: string): string {
});
}
-export function MeetingSessionDetailsScreen({ sessions }: MeetingSessionDetailsScreenProps) {
+export function MeetingSessionDetailsScreen({
+ sessions,
+ transparentBackground = false,
+}: MeetingSessionDetailsScreenProps) {
const insets = useSafeAreaInsets();
+ const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
+ const pillStyle = transparentBackground ? "bg-[#E8E8ED]/50" : "bg-[#E8E8ED]";
const renderSession = ({ item, index }: { item: ConferencingSession; index: number }) => (
-
+
{/* Session header */}
-
+
-
+
@@ -60,10 +72,8 @@ export function MeetingSessionDetailsScreen({ sessions }: MeetingSessionDetailsS
{item.duration && (
-
-
- {formatDuration(item.duration)}
-
+
+ {formatDuration(item.duration)}
)}
@@ -72,40 +82,35 @@ export function MeetingSessionDetailsScreen({ sessions }: MeetingSessionDetailsS
-
- Started: {formatDate(item.startTime)}
-
+ Started: {formatDate(item.startTime)}
{item.endTime && (
-
- Ended: {formatTime(item.endTime)}
-
+ Ended: {formatTime(item.endTime)}
)}
{item.maxParticipants && (
-
- Max participants: {item.maxParticipants}
-
+ Max participants: {item.maxParticipants}
)}
{/* Participants */}
{item.participants && item.participants.length > 0 && (
-
+
Participants ({item.participants.length})
{item.participants.map((participant, pIndex) => (
-
+ className={`mb-2 flex-row items-center p-3 ${
+ transparentBackground ? "" : "rounded-xl bg-[#F2F2F7]"
+ }`}>
+
@@ -126,8 +131,8 @@ export function MeetingSessionDetailsScreen({ sessions }: MeetingSessionDetailsS
if (sessions.length === 0) {
return (
-
-
+
+
@@ -141,13 +146,13 @@ export function MeetingSessionDetailsScreen({ sessions }: MeetingSessionDetailsS
}
return (
-
+
item.id}
- showsVerticalScrollIndicator={false}
+ showsVerticalScrollIndicator={!transparentBackground}
contentContainerStyle={{ paddingBottom: 16 }}
/>
diff --git a/companion/components/screens/RescheduleScreen.android.tsx b/companion/components/screens/RescheduleScreen.android.tsx
new file mode 100644
index 0000000000..e73318a4ce
--- /dev/null
+++ b/companion/components/screens/RescheduleScreen.android.tsx
@@ -0,0 +1,534 @@
+/**
+ * RescheduleScreen Component (Android)
+ *
+ * Android-specific implementation with modal-based date/time picker.
+ * Uses custom modal dialogs with Material Design 3 inspired styling.
+ */
+
+import { Ionicons } from "@expo/vector-icons";
+import { useRouter } from "expo-router";
+import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
+import {
+ Alert,
+ KeyboardAvoidingView,
+ Modal,
+ Pressable,
+ ScrollView,
+ Text,
+ TextInput,
+ View,
+} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { AppPressable } from "@/components/AppPressable";
+import type { Booking } from "@/services/calcom";
+import { CalComAPIService } from "@/services/calcom";
+import { safeLogError, safeLogInfo } from "@/utils/safeLogger";
+
+export interface RescheduleScreenProps {
+ booking: Booking | null;
+ onSuccess: () => void;
+ onSavingChange?: (isSaving: boolean) => void;
+ useNativeHeader?: boolean;
+}
+
+export interface RescheduleScreenHandle {
+ submit: () => void;
+}
+
+// Format time in 12-hour format
+function formatTime12Hour(date: Date): string {
+ const hours = date.getHours();
+ const minutes = date.getMinutes();
+ const ampm = hours >= 12 ? "PM" : "AM";
+ const displayHours = hours % 12 || 12;
+ return `${displayHours}:${String(minutes).padStart(2, "0")} ${ampm}`;
+}
+
+export const RescheduleScreen = forwardRef(
+ function RescheduleScreen({ booking, onSuccess, onSavingChange, useNativeHeader = false }, ref) {
+ const router = useRouter();
+ const insets = useSafeAreaInsets();
+ const [selectedDateTime, setSelectedDateTime] = useState(new Date());
+ const [showDatePicker, setShowDatePicker] = useState(false);
+ const [showTimePicker, setShowTimePicker] = useState(false);
+ const [reason, setReason] = useState("");
+ const [isSaving, setIsSaving] = useState(false);
+
+ useEffect(() => {
+ if (booking?.startTime) {
+ const bookingDate = new Date(booking.startTime);
+ if (!Number.isNaN(bookingDate.getTime())) {
+ setSelectedDateTime(bookingDate);
+ setReason("");
+ }
+ }
+ }, [booking?.startTime]);
+
+ useEffect(() => {
+ onSavingChange?.(isSaving);
+ }, [isSaving, onSavingChange]);
+
+ const handleSubmit = useCallback(async () => {
+ if (!booking) return;
+
+ if (selectedDateTime <= new Date()) {
+ Alert.alert("Error", "Please select a future date and time");
+ return;
+ }
+
+ setIsSaving(true);
+ const reschedulingReason = reason.trim() || undefined;
+ try {
+ await CalComAPIService.rescheduleBooking(booking.uid, {
+ start: selectedDateTime.toISOString(),
+ reschedulingReason,
+ });
+ Alert.alert("Success", "Booking rescheduled successfully", [
+ { text: "OK", onPress: onSuccess },
+ ]);
+ setIsSaving(false);
+ } catch (error) {
+ safeLogError("[RescheduleScreen] Failed to reschedule:", error);
+ Alert.alert("Error", "Failed to reschedule booking. Please try again.");
+ setIsSaving(false);
+ }
+ }, [booking, selectedDateTime, reason, onSuccess]);
+
+ // Format date for display
+ const formattedDate = selectedDateTime.toLocaleDateString(undefined, {
+ weekday: "short",
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ });
+
+ // Format time for display (12-hour)
+ const formattedTime = formatTime12Hour(selectedDateTime);
+
+ // Generate date options for picker (next 90 days)
+ const dateOptions = React.useMemo(() => {
+ const options: { label: string; sublabel: string; value: Date }[] = [];
+ const today = new Date();
+ for (let i = 0; i < 90; i++) {
+ const date = new Date(today);
+ date.setDate(today.getDate() + i);
+ const isToday = i === 0;
+ const isTomorrow = i === 1;
+ options.push({
+ label: isToday
+ ? "Today"
+ : isTomorrow
+ ? "Tomorrow"
+ : date.toLocaleDateString(undefined, {
+ weekday: "long",
+ }),
+ sublabel: date.toLocaleDateString(undefined, {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ }),
+ value: date,
+ });
+ }
+ return options;
+ }, []);
+
+ // Extract booking times for stable dependency references
+ const bookingStartTime = booking?.startTime;
+ const bookingEndTime = booking?.endTime;
+
+ // Calculate booking duration in minutes
+ const bookingDurationMinutes = React.useMemo(() => {
+ if (!bookingStartTime || !bookingEndTime) return 30; // Default to 30 min
+ const start = new Date(bookingStartTime);
+ const end = new Date(bookingEndTime);
+ const durationMs = end.getTime() - start.getTime();
+ const durationMin = Math.round(durationMs / (1000 * 60));
+ // Ensure slot interval is reasonable (minimum 5 min, maximum 60 min)
+ if (durationMin < 5) return 5;
+ if (durationMin > 60) return 60;
+ return durationMin;
+ }, [bookingStartTime, bookingEndTime]);
+
+ // Generate time options based on booking duration
+ const timeOptions = React.useMemo(() => {
+ const options: {
+ label: string;
+ value: { hour: number; minute: number };
+ }[] = [];
+ const interval = bookingDurationMinutes;
+
+ for (let totalMinutes = 0; totalMinutes < 24 * 60; totalMinutes += interval) {
+ const hour = Math.floor(totalMinutes / 60);
+ const minute = totalMinutes % 60;
+ const ampm = hour >= 12 ? "PM" : "AM";
+ const displayHour = hour % 12 || 12;
+ const time = `${displayHour}:${String(minute).padStart(2, "0")} ${ampm}`;
+ options.push({
+ label: time,
+ value: { hour, minute },
+ });
+ }
+ return options;
+ }, [bookingDurationMinutes]);
+
+ useImperativeHandle(
+ ref,
+ () => ({
+ submit: handleSubmit,
+ }),
+ [handleSubmit]
+ );
+
+ if (!booking) {
+ return (
+
+ No booking data
+
+ );
+ }
+
+ return (
+ <>
+
+ {/* Header - only shown when not using native header */}
+ {!useNativeHeader && (
+
+
+ router.back()}
+ style={{
+ width: 40,
+ height: 40,
+ alignItems: "flex-start",
+ justifyContent: "center",
+ }}
+ >
+
+
+
+
+ Reschedule
+
+
+
+
+ Save
+
+
+
+
+ )}
+
+
+ {/* Booking Title Card */}
+
+
+
+
+
+ Rescheduling
+
+ {booking.title}
+
+
+
+
+ {/* Form Card */}
+
+ {/* Date picker trigger */}
+ ({
+ backgroundColor: pressed ? "#F9F9F9" : "transparent",
+ })}
+ onPress={() => {
+ safeLogInfo("[RescheduleScreen] Opening date picker");
+ setShowDatePicker(true);
+ }}
+ disabled={isSaving}
+ >
+
+ New Date
+ {formattedDate}
+
+
+
+
+ {/* Time picker trigger */}
+ ({
+ backgroundColor: pressed ? "#F9F9F9" : "transparent",
+ })}
+ onPress={() => {
+ safeLogInfo("[RescheduleScreen] Opening time picker");
+ setShowTimePicker(true);
+ }}
+ disabled={isSaving}
+ >
+
+ New Time
+ {formattedTime}
+
+
+
+
+ {/* Reason input */}
+
+
+ Reason (optional)
+
+
+
+
+
+
+
+ {/* Date Picker Modal */}
+ setShowDatePicker(false)}
+ >
+
+ setShowDatePicker(false)}
+ />
+
+ {/* Header */}
+
+ setShowDatePicker(false)}>
+ Cancel
+
+ Select Date
+ setShowDatePicker(false)}>
+ Done
+
+
+
+ {/* Date List */}
+
+ {dateOptions.map((option, index) => {
+ const isSelected =
+ option.value.toDateString() === selectedDateTime.toDateString();
+ return (
+ ({
+ backgroundColor: isSelected
+ ? "rgba(0, 122, 255, 0.1)"
+ : pressed
+ ? "#F5F5F5"
+ : "#fff",
+ borderBottomWidth: index < dateOptions.length - 1 ? 1 : 0,
+ borderBottomColor: "#F2F2F7",
+ })}
+ onPress={() => {
+ const newDate = new Date(option.value);
+ newDate.setHours(selectedDateTime.getHours());
+ newDate.setMinutes(selectedDateTime.getMinutes());
+ setSelectedDateTime(newDate);
+ }}
+ >
+
+
+ {option.label}
+
+
+ {option.sublabel}
+
+
+ {isSelected && }
+
+ );
+ })}
+
+
+
+
+
+ {/* Time Picker Modal */}
+ setShowTimePicker(false)}
+ >
+
+ setShowTimePicker(false)}
+ />
+
+ {/* Header */}
+
+ setShowTimePicker(false)}>
+ Cancel
+
+ Select Time
+ setShowTimePicker(false)}>
+ Done
+
+
+
+ {/* Time Grid */}
+
+ {timeOptions.map((option) => {
+ const isSelected =
+ selectedDateTime.getHours() === option.value.hour &&
+ selectedDateTime.getMinutes() === option.value.minute;
+ return (
+ {
+ const newDate = new Date(selectedDateTime);
+ newDate.setHours(option.value.hour);
+ newDate.setMinutes(option.value.minute);
+ setSelectedDateTime(newDate);
+ }}
+ >
+
+
+ {option.label}
+
+
+
+ );
+ })}
+
+
+
+
+ >
+ );
+ }
+);
+
+export default RescheduleScreen;
diff --git a/companion/components/screens/RescheduleScreen.ios.tsx b/companion/components/screens/RescheduleScreen.ios.tsx
new file mode 100644
index 0000000000..a6a8702d9f
--- /dev/null
+++ b/companion/components/screens/RescheduleScreen.ios.tsx
@@ -0,0 +1,241 @@
+/**
+ * RescheduleScreen Component (iOS)
+ *
+ * iOS-specific implementation with native DatePicker for date/time selection.
+ * Uses @expo/ui/swift-ui DatePicker for the native iOS picker.
+ */
+
+import { DatePicker, Host } from "@expo/ui/swift-ui";
+import { Ionicons } from "@expo/vector-icons";
+import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
+import { Alert, KeyboardAvoidingView, ScrollView, Text, TextInput, View } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import type { Booking } from "@/services/calcom";
+import { CalComAPIService } from "@/services/calcom";
+import { safeLogError, safeLogInfo } from "@/utils/safeLogger";
+
+export interface RescheduleScreenProps {
+ booking: Booking | null;
+ onSuccess: () => void;
+ onSavingChange?: (isSaving: boolean) => void;
+ transparentBackground?: boolean;
+ useNativeHeader?: boolean;
+}
+
+export interface RescheduleScreenHandle {
+ submit: () => void;
+}
+
+export const RescheduleScreen = forwardRef(
+ function RescheduleScreen(
+ { booking, onSuccess, onSavingChange, transparentBackground = false },
+ ref
+ ) {
+ "use no memo";
+ const insets = useSafeAreaInsets();
+ const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
+ const [selectedDateTime, setSelectedDateTime] = useState(new Date());
+ const [reason, setReason] = useState("");
+ const [isSaving, setIsSaving] = useState(false);
+
+ useEffect(() => {
+ if (booking?.startTime) {
+ const bookingDate = new Date(booking.startTime);
+ if (!Number.isNaN(bookingDate.getTime())) {
+ setSelectedDateTime(bookingDate);
+ setReason("");
+ }
+ }
+ }, [booking?.startTime]);
+
+ useEffect(() => {
+ onSavingChange?.(isSaving);
+ }, [isSaving, onSavingChange]);
+
+ const handleSubmit = useCallback(async () => {
+ if (!booking) return;
+
+ if (selectedDateTime <= new Date()) {
+ Alert.alert("Error", "Please select a future date and time");
+ return;
+ }
+
+ setIsSaving(true);
+ try {
+ await CalComAPIService.rescheduleBooking(booking.uid, {
+ start: selectedDateTime.toISOString(),
+ reschedulingReason: reason.trim() || undefined,
+ });
+ Alert.alert("Success", "Booking rescheduled successfully", [
+ { text: "OK", onPress: onSuccess },
+ ]);
+ setIsSaving(false);
+ } catch (error) {
+ safeLogError("[RescheduleScreen] Failed to reschedule:", error);
+ Alert.alert("Error", "Failed to reschedule booking. Please try again.");
+ setIsSaving(false);
+ }
+ }, [booking, selectedDateTime, reason, onSuccess]);
+
+ const handleDateSelected = useCallback(
+ (date: Date) => {
+ safeLogInfo("[RescheduleScreen] Date selected:", date);
+ const newDate = new Date(date);
+ newDate.setHours(selectedDateTime.getHours());
+ newDate.setMinutes(selectedDateTime.getMinutes());
+ setSelectedDateTime(newDate);
+ },
+ [selectedDateTime]
+ );
+
+ const handleTimeSelected = useCallback(
+ (date: Date) => {
+ safeLogInfo("[RescheduleScreen] Time selected:", date);
+ const newDate = new Date(selectedDateTime);
+ newDate.setHours(date.getHours());
+ newDate.setMinutes(date.getMinutes());
+ setSelectedDateTime(newDate);
+ },
+ [selectedDateTime]
+ );
+
+ useImperativeHandle(
+ ref,
+ () => ({
+ submit: handleSubmit,
+ }),
+ [handleSubmit]
+ );
+
+ if (!booking) {
+ return (
+
+ No booking data
+
+ );
+ }
+
+ return (
+
+
+ {transparentBackground ? (
+ <>
+ {/* Booking Title */}
+
+ {booking.title}
+
+
+ {/* Date and Time pickers - side by side */}
+
+
+
+
+ at
+
+
+
+
+
+ {/* Reason input */}
+
+ Reason (optional)
+
+
+
+
+ >
+ ) : (
+ <>
+ {/* Booking Title Card */}
+
+
+
+
+
+ Rescheduling
+
+ {booking.title}
+
+
+
+
+ {/* Form Card */}
+
+ {/* Date picker */}
+
+ New Date
+
+
+
+
+
+ {/* Time picker */}
+
+ New Time
+
+
+
+
+
+ {/* Reason input */}
+
+
+ Reason (optional)
+
+
+
+
+ >
+ )}
+
+
+ );
+ }
+);
+
+export default RescheduleScreen;
diff --git a/companion/components/screens/RescheduleScreen.tsx b/companion/components/screens/RescheduleScreen.tsx
index a577ab1c20..bdb2a68fec 100644
--- a/companion/components/screens/RescheduleScreen.tsx
+++ b/companion/components/screens/RescheduleScreen.tsx
@@ -3,6 +3,7 @@
*
* Screen content for rescheduling a booking.
* Used with the reschedule route that has native Stack.Header.
+ * This is the web/extension fallback - uses native HTML inputs on web.
*/
import { Ionicons } from "@expo/vector-icons";
@@ -23,13 +24,14 @@ import type { Booking } from "@/services/calcom";
import { CalComAPIService } from "@/services/calcom";
import { safeLogError, safeLogInfo } from "@/utils/safeLogger";
-// Note: @expo/ui DateTimePicker components are not yet stable
-// Using a simple inline picker approach instead
+const isWeb = Platform.OS === "web";
export interface RescheduleScreenProps {
booking: Booking | null;
onSuccess: () => void;
onSavingChange?: (isSaving: boolean) => void;
+ transparentBackground?: boolean;
+ useNativeHeader?: boolean;
}
// Handle type for parent component to call submit
@@ -38,9 +40,13 @@ export interface RescheduleScreenHandle {
}
export const RescheduleScreen = forwardRef(
- function RescheduleScreen({ booking, onSuccess, onSavingChange }, ref) {
- "use no memo";
+ function RescheduleScreen(
+ { booking, onSuccess, onSavingChange, transparentBackground = false },
+ ref
+ ) {
const insets = useSafeAreaInsets();
+ const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
+ const pillStyle = transparentBackground ? "bg-[#E8E8ED]/50" : "bg-[#E8E8ED]";
const [selectedDateTime, setSelectedDateTime] = useState(new Date());
const [showDatePicker, setShowDatePicker] = useState(false);
const [showTimePicker, setShowTimePicker] = useState(false);
@@ -72,11 +78,16 @@ export const RescheduleScreen = forwardRef 0 ? trimmedReason : undefined;
+ const startTime = selectedDateTime.toISOString();
+
setIsSaving(true);
try {
await CalComAPIService.rescheduleBooking(booking.uid, {
- start: selectedDateTime.toISOString(),
- reschedulingReason: reason.trim() || undefined,
+ start: startTime,
+ reschedulingReason,
});
Alert.alert("Success", "Booking rescheduled successfully", [
{ text: "OK", onPress: onSuccess },
@@ -89,6 +100,14 @@ export const RescheduleScreen = forwardRef {
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
+ 2,
+ "0"
+ )}-${String(date.getDate()).padStart(2, "0")}`;
+ };
+
// Format date for display
const formattedDate = selectedDateTime.toLocaleDateString(undefined, {
year: "numeric",
@@ -125,7 +144,10 @@ export const RescheduleScreen = forwardRef {
- const options: { label: string; value: { hour: number; minute: number } }[] = [];
+ const options: {
+ label: string;
+ value: { hour: number; minute: number };
+ }[] = [];
for (let hour = 0; hour < 24; hour++) {
for (let minute = 0; minute < 60; minute += 15) {
const time = `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
@@ -149,7 +171,7 @@ export const RescheduleScreen = forwardRef
+
No booking data
);
@@ -159,91 +181,204 @@ export const RescheduleScreen = forwardRef
{/* Booking Title Card */}
-
-
-
-
-
- Rescheduling
-
- {booking.title}
-
-
-
-
- {/* Form Card */}
-
- {/* Date picker */}
- {
- safeLogInfo("[RescheduleScreen] Opening date picker");
- setShowDatePicker(true);
- }}
- disabled={isSaving}
- activeOpacity={0.7}
- >
- New Date
-
-
- {formattedDate}
-
-
+ {!transparentBackground && (
+
+
+
-
-
- {/* Time picker */}
- {
- safeLogInfo("[RescheduleScreen] Opening time picker");
- setShowTimePicker(true);
- }}
- disabled={isSaving}
- activeOpacity={0.7}
- >
- New Time
-
-
- {formattedTime}
+
+ Rescheduling
+
+ {booking.title}
-
-
-
- {/* Reason input */}
-
-
- Reason (optional)
-
-
-
+ )}
- {/* Info note */}
-
-
-
- Attendees will receive an email notification about the new time.
-
-
+ {/* Form - Separate cards for transparent mode, grouped for non-transparent */}
+ {transparentBackground ? (
+ <>
+ {/* Date picker - pill button */}
+ {!isWeb && (
+
+ Date
+ {
+ safeLogInfo("[RescheduleScreen] Opening date picker");
+ setShowDatePicker(true);
+ }}
+ disabled={isSaving}
+ activeOpacity={0.7}
+ >
+ {formattedDate}
+
+
+ )}
+
+ {/* Time picker - pill button */}
+ {!isWeb && (
+
+ Time
+ {
+ safeLogInfo("[RescheduleScreen] Opening time picker");
+ setShowTimePicker(true);
+ }}
+ disabled={isSaving}
+ activeOpacity={0.7}
+ >
+ {formattedTime}
+
+
+ )}
+
+ {/* Reason input */}
+
+
+
+ >
+ ) : (
+
+ {/* Date picker */}
+ {isWeb ? (
+
+ New Date
+ {
+ const [year, month, day] = e.target.value.split("-").map(Number);
+ if (!Number.isNaN(year) && !Number.isNaN(month) && !Number.isNaN(day)) {
+ const newDate = new Date(selectedDateTime);
+ newDate.setFullYear(year, month - 1, day);
+ safeLogInfo("[RescheduleScreen] Date selected:", newDate);
+ setSelectedDateTime(newDate);
+ }
+ }}
+ disabled={isSaving}
+ style={{
+ width: "100%",
+ height: 40,
+ fontSize: 17,
+ border: "none",
+ outline: "none",
+ backgroundColor: "transparent",
+ color: "#000",
+ }}
+ min={formatLocalDate(new Date())}
+ />
+
+ ) : (
+
+ New Date
+ {
+ safeLogInfo("[RescheduleScreen] Opening date picker");
+ setShowDatePicker(true);
+ }}
+ disabled={isSaving}
+ activeOpacity={0.7}
+ >
+ {formattedDate}
+
+
+ )}
+
+ {/* Time picker */}
+ {isWeb ? (
+
+ New Time
+ {
+ const [hours, minutes] = e.target.value.split(":").map(Number);
+ if (!Number.isNaN(hours) && !Number.isNaN(minutes)) {
+ safeLogInfo("[RescheduleScreen] Time selected:", {
+ hours,
+ minutes,
+ });
+ const newDate = new Date(selectedDateTime);
+ newDate.setHours(hours);
+ newDate.setMinutes(minutes);
+ setSelectedDateTime(newDate);
+ }
+ }}
+ disabled={isSaving}
+ style={{
+ width: "100%",
+ height: 40,
+ fontSize: 17,
+ border: "none",
+ outline: "none",
+ backgroundColor: "transparent",
+ color: "#000",
+ }}
+ />
+
+ ) : (
+
+ New Time
+ {
+ safeLogInfo("[RescheduleScreen] Opening time picker");
+ setShowTimePicker(true);
+ }}
+ disabled={isSaving}
+ activeOpacity={0.7}
+ >
+ {formattedTime}
+
+
+ )}
+
+ {/* Reason input */}
+
+
+ Reason (optional)
+
+
+
+
+ )}
diff --git a/companion/components/screens/ViewRecordingsScreen.tsx b/companion/components/screens/ViewRecordingsScreen.tsx
index 8947d47f4e..c962323402 100644
--- a/companion/components/screens/ViewRecordingsScreen.tsx
+++ b/companion/components/screens/ViewRecordingsScreen.tsx
@@ -13,6 +13,7 @@ import type { BookingRecording } from "@/services/types/bookings.types";
export interface ViewRecordingsScreenProps {
recordings: BookingRecording[];
+ transparentBackground?: boolean;
}
function formatDuration(seconds?: number): string {
@@ -34,8 +35,13 @@ function formatDate(dateString: string): string {
});
}
-export function ViewRecordingsScreen({ recordings }: ViewRecordingsScreenProps) {
+export function ViewRecordingsScreen({
+ recordings,
+ transparentBackground = false,
+}: ViewRecordingsScreenProps) {
const insets = useSafeAreaInsets();
+ const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
+ const pillStyle = transparentBackground ? "bg-[#E8E8ED]/50" : "bg-[#E8E8ED]";
const handleOpenRecording = async (recording: BookingRecording) => {
try {
@@ -49,12 +55,13 @@ export function ViewRecordingsScreen({ recordings }: ViewRecordingsScreenProps)
const renderRecording = ({ item }: { item: BookingRecording }) => (
handleOpenRecording(item)}
- activeOpacity={0.7}
- >
+ activeOpacity={0.7}>
-
+
@@ -73,13 +80,11 @@ export function ViewRecordingsScreen({ recordings }: ViewRecordingsScreenProps)
if (recordings.length === 0) {
return (
-
-
+
+
-
- No recordings available
-
+ No recordings available
Recordings may take some time to process after the meeting ends.
@@ -88,13 +93,13 @@ export function ViewRecordingsScreen({ recordings }: ViewRecordingsScreenProps)
}
return (
-
+
item.id}
- showsVerticalScrollIndicator={false}
+ showsVerticalScrollIndicator={!transparentBackground}
contentContainerStyle={{ paddingBottom: 16 }}
/>
diff --git a/companion/hooks/useBookingActions.ts b/companion/hooks/useBookingActions.ts
index fdee52d6a5..b80611dbef 100644
--- a/companion/hooks/useBookingActions.ts
+++ b/companion/hooks/useBookingActions.ts
@@ -346,12 +346,16 @@ export const useBookingActions = ({
/**
* Submit rejection with reason (used by iOS custom modal)
+ * @param reasonOverride - Optional reason passed directly to avoid race condition with state updates
*/
- const handleSubmitReject = () => {
+ const handleSubmitReject = (reasonOverride?: string) => {
if (!rejectBooking) return;
+ // Use passed reason if provided, otherwise fall back to state
+ const reason = reasonOverride !== undefined ? reasonOverride : rejectReason;
+
declineMutation(
- { uid: rejectBooking.uid, reason: rejectReason || undefined },
+ { uid: rejectBooking.uid, reason: reason || undefined },
{
onSuccess: () => {
setShowRejectModal(false);
diff --git a/companion/services/calcom.ts b/companion/services/calcom.ts
index 25cc3ce6f0..17ef2881dd 100644
--- a/companion/services/calcom.ts
+++ b/companion/services/calcom.ts
@@ -337,11 +337,15 @@ async function testRawBookingsAPI(): Promise {
if (responseText?.trim()) {
const _responseJson = safeParseJson(responseText);
if (!_responseJson) {
- safeLogError("[CalComAPIService] Failed to parse bookings response", { responseText });
+ safeLogError("[CalComAPIService] Failed to parse bookings response", {
+ responseText,
+ });
}
}
} catch (_error) {
- safeLogError("[CalComAPIService] testRawBookingsAPI failed", { error: _error });
+ safeLogError("[CalComAPIService] testRawBookingsAPI failed", {
+ error: _error,
+ });
}
}
@@ -928,7 +932,7 @@ async function getBookingByUid(bookingUid: string): Promise {
}
throw new Error("Invalid response from get booking API");
} catch (error) {
- console.error("getBookingByUid error");
+ console.error("getBookingByUid error:", error);
throw error;
}
}
@@ -1008,7 +1012,7 @@ async function getBookings(filters?: {
}
// Get current user to filter bookings
- let currentUser;
+ let currentUser: UserProfile | undefined;
try {
currentUser = await getCurrentUser();
} catch (_error) {