diff --git a/.github/workflows/cubic-devin-review.yml b/.github/workflows/cubic-devin-review.yml index d8b0fc6678..b1b7ef7e1c 100644 --- a/.github/workflows/cubic-devin-review.yml +++ b/.github/workflows/cubic-devin-review.yml @@ -187,7 +187,7 @@ jobs: 4. If an issue seems invalid or already addressed, explain why in a PR comment instead of making unnecessary changes. 5. Never ask for user confirmation. Never wait for user messages." - RESPONSE=$(curl -s -X POST "https://api.devin.ai/v1/sessions" \ + HTTP_CODE=$(curl -s -o /tmp/devin-response.json -w "%{http_code}" -X POST "https://api.devin.ai/v1/sessions" \ -H "Authorization: Bearer ${DEVIN_API_KEY}" \ -H "Content-Type: application/json" \ -d "$(jq -n \ @@ -199,7 +199,13 @@ jobs: tags: ["cubic-ai-review", "pr-${{ github.event.pull_request.number }}"] }')") - SESSION_URL=$(echo "$RESPONSE" | jq -r '.url // .session_url // empty') + if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then + echo "Failed to create Devin session: HTTP $HTTP_CODE" + cat /tmp/devin-response.json + exit 1 + fi + + SESSION_URL=$(cat /tmp/devin-response.json | jq -r '.url // .session_url // empty') if [ -n "$SESSION_URL" ]; then echo "Devin session created: $SESSION_URL" echo "SESSION_URL=$SESSION_URL" >> $GITHUB_ENV diff --git a/.github/workflows/devin-conflict-resolver.yml b/.github/workflows/devin-conflict-resolver.yml index d48fd1ff6f..cf48112cfd 100644 --- a/.github/workflows/devin-conflict-resolver.yml +++ b/.github/workflows/devin-conflict-resolver.yml @@ -314,10 +314,10 @@ jobs: If you prefer to resolve conflicts manually, you can close the Devin session and handle it yourself.` }); } else { - console.log(`Failed to get session URL for PR #${pr.number}:`, data); + console.log(`Failed to get session URL for PR #${pr.number}`); } } catch (error) { - console.error(`Error creating Devin session for PR #${pr.number}:`, error); + console.error(`Error creating Devin session for PR #${pr.number}: ${error.message}`); } await new Promise(resolve => setTimeout(resolve, 1000)); diff --git a/.github/workflows/release-docker.yaml b/.github/workflows/release-docker.yaml index b5385d3a53..8b062b7ee2 100644 --- a/.github/workflows/release-docker.yaml +++ b/.github/workflows/release-docker.yaml @@ -65,8 +65,10 @@ jobs: if [ "${{ inputs.BUILD_FROM_BRANCH }}" = "true" ]; then # Extract branch name and short SHA for the tag BRANCH_NAME="${GITHUB_REF#refs/heads/}" - # Sanitize branch name: replace slashes with dashes for valid Docker tag - SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | tr '/' '-') + # Sanitize branch name: replace all invalid Docker tag characters with dashes + # Docker tags can only contain lowercase/uppercase letters, digits, underscores, periods, and dashes + # Also remove any leading dashes or periods + SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | sed 's/[^a-zA-Z0-9_.-]/-/g' | sed 's/^[-.]*//') SHORT_SHA=$(git rev-parse --short HEAD) echo "release_tag=${SANITIZED_BRANCH}-${SHORT_SHA}" >> $GITHUB_OUTPUT else diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index a72987685c..d95352b64a 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2878,9 +2878,9 @@ "time_range": "Time range", "automatically_add_all_team_members": "Add all team members, including future members", "redirect_to": "Redirect to", - "having_trouble_finding_time": "Having trouble finding a time?", - "show_more": "Show more", - "forward_params_redirect": "Forward parameters such as ?email=...&name=.... and more", + "having_trouble_finding_time": "Having trouble finding a time?", + "show_more": "Show more", + "forward_params_redirect": "Forward parameters such as ?email=...&name=.... and more", "assignment_description": "Schedule meetings when everyone is available or rotate through members on your team", "lowest": "lowest", "low": "low", diff --git a/companion/app/(tabs)/(bookings)/booking-detail.ios.tsx b/companion/app/(tabs)/(bookings)/booking-detail.ios.tsx index 407f562823..d36ae2feaa 100644 --- a/companion/app/(tabs)/(bookings)/booking-detail.ios.tsx +++ b/companion/app/(tabs)/(bookings)/booking-detail.ios.tsx @@ -1,11 +1,11 @@ import * as Clipboard from "expo-clipboard"; import { Stack, useLocalSearchParams } from "expo-router"; import { useCallback, useMemo, useRef } from "react"; -import { Alert } from "react-native"; import { BookingDetailScreen } from "@/components/screens/BookingDetailScreen"; import { useAuth } from "@/contexts/AuthContext"; import { useBookingByUid } from "@/hooks/useBookings"; import type { Booking } from "@/services/calcom"; +import { showErrorAlert, showInfoAlert, showSuccessAlert } from "@/utils/alerts"; import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions"; import { openInAppBrowser } from "@/utils/browser"; @@ -94,7 +94,7 @@ export default function BookingDetailIOS() { const handleCopyMeetingLink = useCallback(async () => { if (meetingUrl) { await Clipboard.setStringAsync(meetingUrl); - Alert.alert("Copied", "Meeting link copied to clipboard"); + showSuccessAlert("Copied", "Meeting link copied to clipboard"); } }, [meetingUrl]); @@ -117,7 +117,7 @@ export default function BookingDetailIOS() { if (handlers?.[handlerName]) { (handlers[handlerName] as () => void)(); } else { - Alert.alert("Error", errorMessage); + showErrorAlert("Error", errorMessage); } }, []); @@ -187,7 +187,7 @@ export default function BookingDetailIOS() { handlerName: null, errorMessage: null, customHandler: () => { - Alert.alert("Report Booking", "Report booking functionality is not yet available"); + showInfoAlert("Report Booking", "Report booking functionality is not yet available"); }, destructive: true, gatingKey: null, diff --git a/companion/app/(tabs)/(bookings)/booking-detail.tsx b/companion/app/(tabs)/(bookings)/booking-detail.tsx index 40d7c410e6..27908182ad 100644 --- a/companion/app/(tabs)/(bookings)/booking-detail.tsx +++ b/companion/app/(tabs)/(bookings)/booking-detail.tsx @@ -1,9 +1,9 @@ import { Stack, useLocalSearchParams } from "expo-router"; import { useCallback, useMemo, useRef } from "react"; -import { Alert } from "react-native"; import { BookingDetailScreen } from "@/components/screens/BookingDetailScreen"; import { useAuth } from "@/contexts/AuthContext"; import { useBookingByUid } from "@/hooks/useBookings"; +import { showErrorAlert, showInfoAlert } from "@/utils/alerts"; import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions"; // Empty actions result for when no booking is loaded @@ -64,7 +64,7 @@ export default function BookingDetail() { if (actionHandlersRef.current?.openRescheduleModal) { actionHandlersRef.current.openRescheduleModal(); } else { - Alert.alert("Error", "Unable to reschedule. Please try again."); + showErrorAlert("Error", "Unable to reschedule. Please try again."); } }, []); @@ -72,7 +72,7 @@ export default function BookingDetail() { if (actionHandlersRef.current?.openEditLocationModal) { actionHandlersRef.current.openEditLocationModal(); } else { - Alert.alert("Error", "Unable to edit location. Please try again."); + showErrorAlert("Error", "Unable to edit location. Please try again."); } }, []); @@ -80,7 +80,7 @@ export default function BookingDetail() { if (actionHandlersRef.current?.openAddGuestsModal) { actionHandlersRef.current.openAddGuestsModal(); } else { - Alert.alert("Error", "Unable to add guests. Please try again."); + showErrorAlert("Error", "Unable to add guests. Please try again."); } }, []); @@ -88,7 +88,7 @@ export default function BookingDetail() { if (actionHandlersRef.current?.openViewRecordingsModal) { actionHandlersRef.current.openViewRecordingsModal(); } else { - Alert.alert("Error", "Unable to view recordings. Please try again."); + showErrorAlert("Error", "Unable to view recordings. Please try again."); } }, []); @@ -96,7 +96,7 @@ export default function BookingDetail() { if (actionHandlersRef.current?.openMeetingSessionDetailsModal) { actionHandlersRef.current.openMeetingSessionDetailsModal(); } else { - Alert.alert("Error", "Unable to view session details. Please try again."); + showErrorAlert("Error", "Unable to view session details. Please try again."); } }, []); @@ -104,19 +104,19 @@ export default function BookingDetail() { if (actionHandlersRef.current?.openMarkNoShowModal) { actionHandlersRef.current.openMarkNoShowModal(); } else { - Alert.alert("Error", "Unable to mark no-show. Please try again."); + showErrorAlert("Error", "Unable to mark no-show. Please try again."); } }, []); const handleReport = useCallback(() => { - Alert.alert("Report Booking", "Report booking functionality is not yet available"); + showInfoAlert("Report Booking", "Report booking functionality is not yet available"); }, []); const handleCancel = useCallback(() => { if (actionHandlersRef.current?.handleCancelBooking) { actionHandlersRef.current.handleCancelBooking(); } else { - Alert.alert("Error", "Unable to cancel. Please try again."); + showErrorAlert("Error", "Unable to cancel. Please try again."); } }, []); diff --git a/companion/app/(tabs)/(event-types)/event-type-detail.tsx b/companion/app/(tabs)/(event-types)/event-type-detail.tsx index 8393b3e1b8..b95efcffea 100644 --- a/companion/app/(tabs)/(event-types)/event-type-detail.tsx +++ b/companion/app/(tabs)/(event-types)/event-type-detail.tsx @@ -1,7 +1,7 @@ import { Ionicons } from "@expo/vector-icons"; import * as Clipboard from "expo-clipboard"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Alert, Animated, @@ -35,7 +35,7 @@ import { type Schedule, } from "@/services/calcom"; import type { LocationItem, LocationOptionGroup } from "@/types/locations"; -import { showErrorAlert } from "@/utils/alerts"; +import { showErrorAlert, showInfoAlert, showSuccessAlert } from "@/utils/alerts"; import { openInAppBrowser } from "@/utils/browser"; import { buildLocationOptions, @@ -44,7 +44,7 @@ import { validateLocationItem, } from "@/utils/locationHelpers"; import { safeLogError } from "@/utils/safeLogger"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; +import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect"; // Type definitions for extended EventType fields not in the base type interface EventTypeExtended { @@ -163,7 +163,15 @@ export default function EventTypeDetail() { const [showScheduleDropdown, setShowScheduleDropdown] = useState(false); const [schedulesLoading, setSchedulesLoading] = useState(false); const [scheduleDetailsLoading, setScheduleDetailsLoading] = useState(false); - const [isHidden, setIsHidden] = useState(false); + const [initialScheduleId, setInitialScheduleId] = useState(null); + const hasAutoSelectedScheduleRef = useRef(false); + + // Reset auto-selection ref when event type id changes (e.g., navigation reuse) + useEffect(() => { + hasAutoSelectedScheduleRef.current = false; + }, [id]); + + const [isHidden, setIsHidden] = useState(false); const [selectedTimezone, setSelectedTimezone] = useState(""); const [showTimezoneDropdown, setShowTimezoneDropdown] = useState(false); const [conferencingOptions, setConferencingOptions] = useState([]); @@ -400,18 +408,32 @@ export default function EventTypeDetail() { const schedulesData = await CalComAPIService.getSchedules(); setSchedules(schedulesData); - // Set default schedule if one exists - const defaultSchedule = schedulesData.find((schedule) => schedule.isDefault); - if (defaultSchedule) { - setSelectedSchedule(defaultSchedule); - await fetchScheduleDetails(defaultSchedule.id); + // Only auto-select a schedule if we haven't already done so + // This prevents unnecessary re-fetches when the callback is recreated + if (!hasAutoSelectedScheduleRef.current) { + // Use the event type's schedule if we have it, otherwise use default + let scheduleToSelect: Schedule | undefined; + + if (initialScheduleId) { + scheduleToSelect = schedulesData.find((schedule) => schedule.id === initialScheduleId); + } + + if (!scheduleToSelect) { + scheduleToSelect = schedulesData.find((schedule) => schedule.isDefault); + } + + if (scheduleToSelect) { + hasAutoSelectedScheduleRef.current = true; + setSelectedSchedule(scheduleToSelect); + await fetchScheduleDetails(scheduleToSelect.id); + } } setSchedulesLoading(false); } catch (error) { safeLogError("Failed to fetch schedules:", error); setSchedulesLoading(false); } - }, [fetchScheduleDetails]); + }, [fetchScheduleDetails, initialScheduleId]); const fetchConferencingOptions = useCallback(async () => { setConferencingLoading(true); @@ -435,6 +457,11 @@ export default function EventTypeDetail() { if (eventType.lengthInMinutes) setEventDuration(eventType.lengthInMinutes.toString()); if (eventType.hidden !== undefined) setIsHidden(eventType.hidden); + // Load schedule ID - this will be used by fetchSchedules to select the correct schedule + if (eventType.scheduleId) { + setInitialScheduleId(eventType.scheduleId); + } + const eventTypeExt = eventType as EventType & EventTypeExtended; const lengthOptions = eventTypeExt.lengthInMinutesOptions; const hasLengthOptions = @@ -941,7 +968,7 @@ export default function EventTypeDetail() { return; } await Clipboard.setStringAsync(link); - Alert.alert("Success", "Link copied!"); + showSuccessAlert("Success", "Link copied!"); }; const handleDelete = () => { @@ -961,12 +988,8 @@ export default function EventTypeDetail() { try { await CalComAPIService.deleteEventType(eventTypeId); - Alert.alert("Success", "Event type deleted successfully", [ - { - text: "OK", - onPress: () => router.back(), - }, - ]); + showSuccessAlert("Success", "Event type deleted successfully"); + router.back(); } catch (error) { safeLogError("Failed to delete event type:", error); showErrorAlert("Error", "Failed to delete event type. Please try again."); @@ -1143,19 +1166,19 @@ export default function EventTypeDetail() { const handleSave = async () => { if (!id) { - Alert.alert("Error", "Event type ID is missing"); + showErrorAlert("Error", "Event type ID is missing"); return; } // Validate required fields if (!eventTitle || !eventSlug) { - Alert.alert("Error", "Title and slug are required"); + showErrorAlert("Error", "Title and slug are required"); return; } const durationNum = parseInt(eventDuration, 10); if (Number.isNaN(durationNum) || durationNum <= 0) { - Alert.alert("Error", "Duration must be a positive number"); + showErrorAlert("Error", "Duration must be a positive number"); return; } @@ -1164,7 +1187,7 @@ export default function EventTypeDetail() { for (const loc of locations) { const validation = validateLocationItem(loc); if (!validation.valid) { - Alert.alert("Error", validation.error || "Invalid location"); + showErrorAlert("Error", validation.error || "Invalid location"); return; } } @@ -1209,12 +1232,8 @@ export default function EventTypeDetail() { setSaving(false); return; } - Alert.alert("Success", "Event type created successfully", [ - { - text: "OK", - onPress: () => router.back(), - }, - ]); + showSuccessAlert("Success", "Event type created successfully"); + router.back(); setSaving(false); } else { // For UPDATE mode, use partial update - only send changed fields @@ -1236,7 +1255,7 @@ export default function EventTypeDetail() { setSaving(false); return; } - Alert.alert("Success", "Event type updated successfully"); + showSuccessAlert("Success", "Event type updated successfully"); // Refresh event type data to sync with server await fetchEventTypeData(); setSaving(false); @@ -1249,7 +1268,7 @@ export default function EventTypeDetail() { const renderHeaderLeft = () => ( router.back()} className="px-2 py-2"> - + ); @@ -1261,13 +1280,13 @@ export default function EventTypeDetail() { - + {tabs.find((tab) => tab.id === activeTab)?.label ?? "Basics"} @@ -1287,11 +1306,11 @@ export default function EventTypeDetail() { {tab.label} @@ -1311,7 +1330,7 @@ export default function EventTypeDetail() { > {saveButtonText} @@ -1405,6 +1424,7 @@ export default function EventTypeDetail() { selectedDurations={selectedDurations} setShowDurationDropdown={setShowDurationDropdown} defaultDuration={defaultDuration} + setDefaultDuration={setDefaultDuration} setShowDefaultDurationDropdown={setShowDefaultDurationDropdown} // Multiple locations support locations={locations} @@ -1424,41 +1444,88 @@ export default function EventTypeDetail() { onRequestClose={() => setShowDurationDropdown(false)} > setShowDurationDropdown(false)} > - - - Select Available Durations - - - {availableDurations.map((duration) => ( - toggleDurationSelection(duration)} - > - - {duration} + + {isLiquidGlassAvailable() && Platform.OS === "ios" ? ( + + + + Select Available Durations - {selectedDurations.includes(duration) ? ( - - ) : null} + + {availableDurations.map((duration, index) => ( + toggleDurationSelection(duration)} + > + + {duration} + + {selectedDurations.includes(duration) ? ( + + ) : ( + + )} + + ))} + + setShowDurationDropdown(false)} + > + Done + + + + ) : ( + + + Select Available Durations + + + {availableDurations.map((duration, index) => ( + toggleDurationSelection(duration)} + > + + {duration} + + {selectedDurations.includes(duration) ? ( + + ) : ( + + )} + + ))} + + setShowDurationDropdown(false)} + > + Done - ))} - - setShowDurationDropdown(false)} - > - Done - + + )} @@ -1471,36 +1538,76 @@ export default function EventTypeDetail() { onRequestClose={() => setShowDefaultDurationDropdown(false)} > setShowDefaultDurationDropdown(false)} > - - - Select Default Duration - - {selectedDurations.map((duration) => ( - { - setDefaultDuration(duration); - setShowDefaultDurationDropdown(false); - }} - > - - {duration} + + {isLiquidGlassAvailable() && Platform.OS === "ios" ? ( + + + + Select Default Duration + + + {selectedDurations.map((duration, index) => ( + { + setDefaultDuration(duration); + setShowDefaultDurationDropdown(false); + }} + > + + {duration} + + {defaultDuration === duration ? ( + + ) : null} + + ))} + + + + ) : ( + + + Select Default Duration - {defaultDuration === duration ? ( - - ) : null} - - ))} + + {selectedDurations.map((duration, index) => ( + { + setDefaultDuration(duration); + setShowDefaultDurationDropdown(false); + }} + > + + {duration} + + {defaultDuration === duration ? ( + + ) : null} + + ))} + + + )} @@ -1513,44 +1620,88 @@ export default function EventTypeDetail() { onRequestClose={() => setShowScheduleDropdown(false)} > setShowScheduleDropdown(false)} > - - - Select Schedule - - {schedules.map((schedule) => ( - { - setSelectedSchedule(schedule); - setShowScheduleDropdown(false); - fetchScheduleDetails(schedule.id); - }} - > - - - {schedule.name} + + {isLiquidGlassAvailable() && Platform.OS === "ios" ? ( + + + + Select Schedule - {schedule.isDefault ? ( - - Default - - ) : null} + + {schedules.map((schedule, index) => ( + { + setSelectedSchedule(schedule); + fetchScheduleDetails(schedule.id); + setShowScheduleDropdown(false); + }} + > + + + {schedule.name} + + {schedule.isDefault && ( + Default + )} + + {selectedSchedule?.id === schedule.id ? ( + + ) : null} + + ))} + - {selectedSchedule?.id === schedule.id ? ( - - ) : null} - - ))} + + ) : ( + + + Select Schedule + + + {schedules.map((schedule, index) => ( + { + setSelectedSchedule(schedule); + fetchScheduleDetails(schedule.id); + setShowScheduleDropdown(false); + }} + > + + + {schedule.name} + + {schedule.isDefault && ( + Default + )} + + {selectedSchedule?.id === schedule.id ? ( + + ) : null} + + ))} + + + )} @@ -1934,20 +2085,26 @@ export default function EventTypeDetail() { getDaySchedules={getDaySchedule} formatTime={formatTime} selectedTimezone={selectedTimezone} + schedules={schedules} + setSelectedSchedule={setSelectedSchedule} /> ) : null} {activeTab === "limits" ? ( - Additional Settings - - Manage these settings on the web for full functionality. - - - - {/* Apps */} - { - if (id === "new") { - Alert.alert("Info", "Save the event type first to configure this setting."); - } else { - openInAppBrowser( - `https://app.cal.com/event-types/${id}?tabName=apps`, - "Apps settings" - ); - } - }} - className="flex-row items-center justify-between bg-white px-4 py-4 active:bg-[#F8F9FA]" - style={{ borderBottomWidth: 1, borderBottomColor: "#E5E5EA" }} + + {/* Configure on Web */} + + - - - - - - Apps - Manage app integrations - + Configure on Web + + + {/* Apps */} + + { + if (id === "new") { + showInfoAlert( + "Info", + "Save the event type first to configure this setting." + ); + } else { + openInAppBrowser( + `https://app.cal.com/event-types/${id}?tabName=apps`, + "Apps settings" + ); + } + }} + activeOpacity={0.5} + > + + + + + + Apps + Manage integrations + + + + - - - {/* Workflows */} - { - if (id === "new") { - Alert.alert("Info", "Save the event type first to configure this setting."); - } else { - openInAppBrowser( - `https://app.cal.com/event-types/${id}?tabName=workflows`, - "Workflows settings" - ); - } - }} - className="flex-row items-center justify-between bg-white px-4 py-4 active:bg-[#F8F9FA]" - style={{ borderBottomWidth: 1, borderBottomColor: "#E5E5EA" }} - > - - - - - - Workflows - Configure automated actions - + {/* Workflows */} + + { + if (id === "new") { + showInfoAlert( + "Info", + "Save the event type first to configure this setting." + ); + } else { + openInAppBrowser( + `https://app.cal.com/event-types/${id}?tabName=workflows`, + "Workflows settings" + ); + } + }} + activeOpacity={0.5} + > + + + + + + Workflows + Automated actions + + + + - - - {/* Webhooks */} - { - if (id === "new") { - Alert.alert("Info", "Save the event type first to configure this setting."); - } else { - openInAppBrowser( - `https://app.cal.com/event-types/${id}?tabName=webhooks`, - "Webhooks settings" - ); - } - }} - className="flex-row items-center justify-between bg-white px-4 py-4 active:bg-[#F8F9FA]" - > - - - - - - Webhooks - Set up event notifications - + {/* Webhooks */} + + { + if (id === "new") { + showInfoAlert( + "Info", + "Save the event type first to configure this setting." + ); + } else { + openInAppBrowser( + `https://app.cal.com/event-types/${id}?tabName=webhooks`, + "Webhooks settings" + ); + } + }} + activeOpacity={0.5} + > + + + + + + Webhooks + Event notifications + + + + - - + ) : null} {activeTab === "basics" && ( - - - Hidden - + + {/* Visibility */} + + + Visibility + + + + + Hidden + + + + - - Preview - - + {/* Quick Actions */} + + + Quick Actions + + + + + Preview + + + + + + Copy Link + + + + + - - Copy Link - - - - - Delete - - + {/* Danger Zone */} + + + + + Delete Event Type + + + + + )} diff --git a/companion/app/(tabs)/(event-types)/index.ios.tsx b/companion/app/(tabs)/(event-types)/index.ios.tsx index ec74ce9649..5f2f19e2c8 100644 --- a/companion/app/(tabs)/(event-types)/index.ios.tsx +++ b/companion/app/(tabs)/(event-types)/index.ios.tsx @@ -28,7 +28,7 @@ import { } from "@/hooks"; import { useEventTypeFilter } from "@/hooks/useEventTypeFilter"; import { CalComAPIService, type EventType } from "@/services/calcom"; -import { showErrorAlert } from "@/utils/alerts"; +import { showErrorAlert, showSuccessAlert } from "@/utils/alerts"; import { openInAppBrowser } from "@/utils/browser"; import { getAvatarUrl } from "@/utils/getAvatarUrl"; import { getEventDuration } from "@/utils/getEventDuration"; @@ -113,7 +113,7 @@ export default function EventTypesIOS() { try { const link = await CalComAPIService.buildEventTypeLink(eventType.slug); await Clipboard.setStringAsync(link); - Alert.alert("Link Copied", "Event type link copied!"); + showSuccessAlert("Link Copied", "Event type link copied!"); } catch { showErrorAlert("Error", "Failed to copy link. Please try again."); } @@ -160,7 +160,7 @@ export default function EventTypesIOS() { onPress: () => { deleteEventTypeMutation(eventType.id, { onSuccess: () => { - Alert.alert("Success", "Event type deleted successfully"); + showSuccessAlert("Success", "Event type deleted successfully"); }, onError: (deleteError) => { const message = @@ -184,7 +184,7 @@ export default function EventTypesIOS() { { eventType, existingEventTypes: eventTypes }, { onSuccess: (duplicatedEventType) => { - Alert.alert("Success", "Event type duplicated successfully"); + showSuccessAlert("Success", "Event type duplicated successfully"); const duration = getEventDuration(eventType); @@ -243,13 +243,13 @@ export default function EventTypesIOS() { text: "Continue", onPress: (title?: string) => { if (!title?.trim()) { - Alert.alert("Error", "Please enter a title for your event type"); + showErrorAlert("Error", "Please enter a title for your event type"); return; } const autoSlug = slugify(title.trim()); if (!autoSlug) { - Alert.alert("Error", "Title must contain at least one letter or number"); + showErrorAlert("Error", "Title must contain at least one letter or number"); return; } diff --git a/companion/app/(tabs)/(event-types)/index.tsx b/companion/app/(tabs)/(event-types)/index.tsx index d966287e7b..e1ea7d144d 100644 --- a/companion/app/(tabs)/(event-types)/index.tsx +++ b/companion/app/(tabs)/(event-types)/index.tsx @@ -38,7 +38,7 @@ import { } from "@/hooks"; import { useEventTypeFilter } from "@/hooks/useEventTypeFilter"; import { CalComAPIService, type EventType } from "@/services/calcom"; -import { showErrorAlert } from "@/utils/alerts"; +import { showErrorAlert, showSuccessAlert } from "@/utils/alerts"; import { openInAppBrowser } from "@/utils/browser"; import { getEventDuration } from "@/utils/getEventDuration"; import { offlineAwareRefresh } from "@/utils/network"; @@ -91,18 +91,6 @@ export default function EventTypes() { const [showDeleteModal, setShowDeleteModal] = useState(false); const [eventTypeToDelete, setEventTypeToDelete] = useState(null); - // Toast state for web platform - const [showToast, setShowToast] = useState(false); - const [toastMessage, setToastMessage] = useState(""); - // Function to show toast - const showToastMessage = (message: string) => { - setToastMessage(message); - setShowToast(true); - setTimeout(() => { - setShowToast(false); - }, 2000); - }; - // Handle pull-to-refresh // Handle pull-to-refresh (offline-aware) const onRefresh = () => offlineAwareRefresh(refetch); @@ -149,17 +137,9 @@ export default function EventTypes() { const link = await CalComAPIService.buildEventTypeLink(eventType.slug); await Clipboard.setStringAsync(link); - if (Platform.OS === "web") { - showToastMessage("Link copied!"); - } else { - Alert.alert("Link Copied", "Event type link copied!"); - } + showSuccessAlert("Link Copied", "Event type link copied!"); } catch { - if (Platform.OS === "web") { - showToastMessage("Failed to copy link"); - } else { - showErrorAlert("Error", "Failed to copy link. Please try again."); - } + showErrorAlert("Error", "Failed to copy link. Please try again."); } }; @@ -205,7 +185,7 @@ export default function EventTypes() { onPress: () => { deleteEventTypeMutation(eventType.id, { onSuccess: () => { - Alert.alert("Success", "Event type deleted successfully"); + showSuccessAlert("Success", "Event type deleted successfully"); }, onError: (error) => { const message = error instanceof Error ? error.message : String(error); @@ -241,11 +221,7 @@ export default function EventTypes() { setShowDeleteModal(false); setEventTypeToDelete(null); - if (Platform.OS === "web") { - showToastMessage("Event type deleted successfully"); - } else { - Alert.alert("Success", "Event type deleted successfully"); - } + showSuccessAlert("Success", "Event type deleted successfully"); }, onError: (error) => { const message = error instanceof Error ? error.message : String(error); @@ -257,11 +233,7 @@ export default function EventTypes() { stack, }); } - if (Platform.OS === "web") { - showToastMessage("Failed to delete event type"); - } else { - showErrorAlert("Error", "Failed to delete event type. Please try again."); - } + showErrorAlert("Error", "Failed to delete event type. Please try again."); }, }); }; @@ -271,11 +243,7 @@ export default function EventTypes() { { eventType, existingEventTypes: eventTypes }, { onSuccess: (duplicatedEventType) => { - if (Platform.OS === "web") { - showToastMessage("Event type duplicated successfully"); - } else { - Alert.alert("Success", "Event type duplicated successfully"); - } + showSuccessAlert("Success", "Event type duplicated successfully"); const duration = getEventDuration(eventType); @@ -305,11 +273,7 @@ export default function EventTypes() { stack, }); } - if (Platform.OS === "web") { - showToastMessage("Failed to duplicate event type"); - } else { - showErrorAlert("Error", "Failed to duplicate event type. Please try again."); - } + showErrorAlert("Error", "Failed to duplicate event type. Please try again."); }, } ); @@ -327,11 +291,7 @@ export default function EventTypes() { } } catch { console.error("Failed to open preview"); - if (Platform.OS === "web") { - showToastMessage("Failed to open preview"); - } else { - showErrorAlert("Error", "Failed to open preview. Please try again."); - } + showErrorAlert("Error", "Failed to open preview. Please try again."); } }; @@ -354,11 +314,11 @@ export default function EventTypes() { setTitleError(""); if (!newEventTitle.trim()) { - // Use inline error for Android AlertDialog, Alert for others + // Use inline error for Android AlertDialog, showErrorAlert for others if (Platform.OS === "android") { setTitleError("Please enter a title for your event type"); } else { - Alert.alert("Error", "Please enter a title for your event type"); + showErrorAlert("Error", "Please enter a title for your event type"); } return; } @@ -368,7 +328,7 @@ export default function EventTypes() { if (Platform.OS === "android") { setTitleError("Title must contain at least one letter or number"); } else { - Alert.alert("Error", "Title must contain at least one letter or number"); + showErrorAlert("Error", "Title must contain at least one letter or number"); } return; } @@ -930,15 +890,6 @@ export default function EventTypes() { - - {/* Toast for Web Platform */} - {showToast ? ( - - - {toastMessage} - - - ) : null} ); } diff --git a/companion/app/_layout.tsx b/companion/app/_layout.tsx index c34dbaeaab..6815b12463 100644 --- a/companion/app/_layout.tsx +++ b/companion/app/_layout.tsx @@ -4,8 +4,10 @@ import { Stack } from "expo-router"; import { Platform, StatusBar, View } from "react-native"; import LoginScreenComponent from "@/components/LoginScreen"; import { NetworkStatusBanner } from "@/components/NetworkStatusBanner"; +import { GlobalToast } from "@/components/ui/GlobalToast"; import { AuthProvider, useAuth } from "@/contexts/AuthContext"; import { QueryProvider } from "@/contexts/QueryContext"; +import { ToastProvider } from "@/contexts/ToastContext"; import "../global.css"; function RootLayoutContent() { @@ -339,7 +341,10 @@ export default function RootLayout() { return ( - + + + + ); diff --git a/companion/app/add-guests.ios.tsx b/companion/app/add-guests.ios.tsx index d94c585db5..f22b7e721a 100644 --- a/companion/app/add-guests.ios.tsx +++ b/companion/app/add-guests.ios.tsx @@ -2,11 +2,12 @@ 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 { ActivityIndicator, 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"; +import { showErrorAlert } from "@/utils/alerts"; // Semi-transparent background to prevent black flash while preserving glass effect const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)"; @@ -35,13 +36,13 @@ export default function AddGuestsIOS() { CalComAPIService.getBookingByUid(uid) .then(setBooking) .catch(() => { - Alert.alert("Error", "Failed to load booking details"); + showErrorAlert("Error", "Failed to load booking details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/add-guests.tsx b/companion/app/add-guests.tsx index dbb88a44c6..2a85711266 100644 --- a/companion/app/add-guests.tsx +++ b/companion/app/add-guests.tsx @@ -1,12 +1,13 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Alert, Platform, View } from "react-native"; +import { ActivityIndicator, Platform, View } from "react-native"; import { AppPressable } from "@/components/AppPressable"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import type { AddGuestsScreenHandle } from "@/components/screens/AddGuestsScreen"; import AddGuestsScreenComponent from "@/components/screens/AddGuestsScreen"; import { type Booking, CalComAPIService } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; export default function AddGuests() { const { uid } = useLocalSearchParams<{ uid: string }>(); @@ -23,13 +24,13 @@ export default function AddGuests() { CalComAPIService.getBookingByUid(uid) .then(setBooking) .catch(() => { - Alert.alert("Error", "Failed to load booking details"); + showErrorAlert("Error", "Failed to load booking details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/edit-availability-day.ios.tsx b/companion/app/edit-availability-day.ios.tsx index 67c8a645dc..b71df2cc28 100644 --- a/companion/app/edit-availability-day.ios.tsx +++ b/companion/app/edit-availability-day.ios.tsx @@ -2,11 +2,12 @@ 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 { ActivityIndicator, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import type { EditAvailabilityDayScreenHandle } from "@/components/screens/EditAvailabilityDayScreen.ios"; import EditAvailabilityDayScreenComponent from "@/components/screens/EditAvailabilityDayScreen.ios"; import { CalComAPIService, type Schedule } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; @@ -39,13 +40,13 @@ export default function EditAvailabilityDayIOS() { CalComAPIService.getScheduleById(Number(id)) .then(setSchedule) .catch(() => { - Alert.alert("Error", "Failed to load schedule details"); + showErrorAlert("Error", "Failed to load schedule details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Schedule ID is missing"); + showErrorAlert("Error", "Schedule ID is missing"); router.back(); } }, [id, router]); diff --git a/companion/app/edit-availability-day.tsx b/companion/app/edit-availability-day.tsx index 852f34f669..e4df6513d3 100644 --- a/companion/app/edit-availability-day.tsx +++ b/companion/app/edit-availability-day.tsx @@ -1,11 +1,12 @@ import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Alert, Text, View } from "react-native"; +import { ActivityIndicator, Text, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import type { EditAvailabilityDayScreenHandle } from "@/components/screens/EditAvailabilityDayScreen"; import EditAvailabilityDayScreenComponent from "@/components/screens/EditAvailabilityDayScreen"; import { CalComAPIService, type Schedule } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; @@ -28,13 +29,13 @@ export default function EditAvailabilityDay() { CalComAPIService.getScheduleById(Number(id)) .then(setSchedule) .catch(() => { - Alert.alert("Error", "Failed to load schedule details"); + showErrorAlert("Error", "Failed to load schedule details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Schedule ID is missing"); + showErrorAlert("Error", "Schedule ID is missing"); router.back(); } }, [id, router]); @@ -73,7 +74,7 @@ export default function EditAvailabilityDay() { isSaving ? "text-gray-400" : "text-[#007AFF]" }`} > - {isSaving ? "Saving..." : "Save"} + Save ), diff --git a/companion/app/edit-availability-hours.ios.tsx b/companion/app/edit-availability-hours.ios.tsx index a6fbbcb62e..3401359252 100644 --- a/companion/app/edit-availability-hours.ios.tsx +++ b/companion/app/edit-availability-hours.ios.tsx @@ -2,10 +2,11 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useState } from "react"; -import { ActivityIndicator, Alert, View } from "react-native"; +import { ActivityIndicator, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import EditAvailabilityHoursScreenComponent from "@/components/screens/EditAvailabilityHoursScreen.ios"; import { CalComAPIService, type Schedule } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; // Semi-transparent background to prevent black flash while preserving glass effect const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)"; @@ -30,13 +31,13 @@ export default function EditAvailabilityHoursIOS() { CalComAPIService.getScheduleById(Number(id)) .then(setSchedule) .catch(() => { - Alert.alert("Error", "Failed to load schedule details"); + showErrorAlert("Error", "Failed to load schedule details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Schedule ID is missing"); + showErrorAlert("Error", "Schedule ID is missing"); router.back(); } }, [id, router]); diff --git a/companion/app/edit-availability-hours.tsx b/companion/app/edit-availability-hours.tsx index be67be47da..6b586c8fa2 100644 --- a/companion/app/edit-availability-hours.tsx +++ b/companion/app/edit-availability-hours.tsx @@ -1,11 +1,12 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useState } from "react"; -import { ActivityIndicator, Alert, TouchableOpacity, View } from "react-native"; +import { ActivityIndicator, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import EditAvailabilityHoursScreenComponent from "@/components/screens/EditAvailabilityHoursScreen"; import { CalComAPIService, type Schedule } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; export default function EditAvailabilityHours() { const { id } = useLocalSearchParams<{ id: string }>(); @@ -20,13 +21,13 @@ export default function EditAvailabilityHours() { CalComAPIService.getScheduleById(Number(id)) .then(setSchedule) .catch(() => { - Alert.alert("Error", "Failed to load schedule details"); + showErrorAlert("Error", "Failed to load schedule details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Schedule ID is missing"); + showErrorAlert("Error", "Schedule ID is missing"); router.back(); } }, [id, router]); diff --git a/companion/app/edit-availability-name.ios.tsx b/companion/app/edit-availability-name.ios.tsx index dcbb39b194..b5f289b88b 100644 --- a/companion/app/edit-availability-name.ios.tsx +++ b/companion/app/edit-availability-name.ios.tsx @@ -2,11 +2,12 @@ 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 { ActivityIndicator, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import type { EditAvailabilityNameScreenHandle } from "@/components/screens/EditAvailabilityNameScreen.ios"; import EditAvailabilityNameScreenComponent from "@/components/screens/EditAvailabilityNameScreen.ios"; import { CalComAPIService, type Schedule } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; // Semi-transparent background to prevent black flash while preserving glass effect const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)"; @@ -34,13 +35,13 @@ export default function EditAvailabilityNameIOS() { CalComAPIService.getScheduleById(Number(id)) .then(setSchedule) .catch(() => { - Alert.alert("Error", "Failed to load schedule details"); + showErrorAlert("Error", "Failed to load schedule details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Schedule ID is missing"); + showErrorAlert("Error", "Schedule ID is missing"); router.back(); } }, [id, router]); diff --git a/companion/app/edit-availability-name.tsx b/companion/app/edit-availability-name.tsx index 9ab548ff91..bc7244c8b3 100644 --- a/companion/app/edit-availability-name.tsx +++ b/companion/app/edit-availability-name.tsx @@ -1,12 +1,13 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Alert, Text, TouchableOpacity, View } from "react-native"; +import { ActivityIndicator, Text, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import type { EditAvailabilityNameScreenHandle } from "@/components/screens/EditAvailabilityNameScreen"; import EditAvailabilityNameScreenComponent from "@/components/screens/EditAvailabilityNameScreen"; import { CalComAPIService, type Schedule } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; export default function EditAvailabilityName() { const { id } = useLocalSearchParams<{ id: string }>(); @@ -24,13 +25,13 @@ export default function EditAvailabilityName() { CalComAPIService.getScheduleById(Number(id)) .then(setSchedule) .catch(() => { - Alert.alert("Error", "Failed to load schedule details"); + showErrorAlert("Error", "Failed to load schedule details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Schedule ID is missing"); + showErrorAlert("Error", "Schedule ID is missing"); router.back(); } }, [id, router]); @@ -92,7 +93,7 @@ export default function EditAvailabilityName() { isSaving ? "text-gray-400" : "text-[#007AFF]" }`} > - {isSaving ? "Saving..." : "Save"} + Save ), diff --git a/companion/app/edit-availability-override.ios.tsx b/companion/app/edit-availability-override.ios.tsx index c288131cc1..326c8208dd 100644 --- a/companion/app/edit-availability-override.ios.tsx +++ b/companion/app/edit-availability-override.ios.tsx @@ -2,11 +2,12 @@ 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 { ActivityIndicator, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import type { EditAvailabilityOverrideScreenHandle } from "@/components/screens/EditAvailabilityOverrideScreen.ios"; import EditAvailabilityOverrideScreenComponent from "@/components/screens/EditAvailabilityOverrideScreen.ios"; import { CalComAPIService, type Schedule } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; // Semi-transparent background to prevent black flash while preserving glass effect const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)"; @@ -40,13 +41,13 @@ export default function EditAvailabilityOverrideIOS() { CalComAPIService.getScheduleById(Number(id)) .then(setSchedule) .catch(() => { - Alert.alert("Error", "Failed to load schedule details"); + showErrorAlert("Error", "Failed to load schedule details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Schedule ID is missing"); + showErrorAlert("Error", "Schedule ID is missing"); router.back(); } }, [id, router]); diff --git a/companion/app/edit-availability-override.tsx b/companion/app/edit-availability-override.tsx index 95196eeb68..79a39c912a 100644 --- a/companion/app/edit-availability-override.tsx +++ b/companion/app/edit-availability-override.tsx @@ -1,12 +1,13 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Alert, Text, TouchableOpacity, View } from "react-native"; +import { ActivityIndicator, Text, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import type { EditAvailabilityOverrideScreenHandle } from "@/components/screens/EditAvailabilityOverrideScreen"; import EditAvailabilityOverrideScreenComponent from "@/components/screens/EditAvailabilityOverrideScreen"; import { CalComAPIService, type Schedule } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; export default function EditAvailabilityOverride() { const { id, overrideIndex } = useLocalSearchParams<{ @@ -30,13 +31,13 @@ export default function EditAvailabilityOverride() { CalComAPIService.getScheduleById(Number(id)) .then(setSchedule) .catch(() => { - Alert.alert("Error", "Failed to load schedule details"); + showErrorAlert("Error", "Failed to load schedule details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Schedule ID is missing"); + showErrorAlert("Error", "Schedule ID is missing"); router.back(); } }, [id, router]); @@ -109,7 +110,7 @@ export default function EditAvailabilityOverride() { isSaving ? "text-gray-400" : "text-[#007AFF]" }`} > - {isSaving ? "Saving..." : "Save"} + Save ), diff --git a/companion/app/edit-location.ios.tsx b/companion/app/edit-location.ios.tsx index e823988b32..cf22e26672 100644 --- a/companion/app/edit-location.ios.tsx +++ b/companion/app/edit-location.ios.tsx @@ -2,11 +2,12 @@ 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 { ActivityIndicator, 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"; +import { showErrorAlert } from "@/utils/alerts"; // Semi-transparent background to prevent black flash while preserving glass effect const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)"; @@ -34,13 +35,13 @@ export default function EditLocationIOS() { CalComAPIService.getBookingByUid(uid) .then(setBooking) .catch(() => { - Alert.alert("Error", "Failed to load booking details"); + showErrorAlert("Error", "Failed to load booking details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/edit-location.tsx b/companion/app/edit-location.tsx index b16c9713b5..96ea6226bf 100644 --- a/companion/app/edit-location.tsx +++ b/companion/app/edit-location.tsx @@ -1,12 +1,13 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Alert, Platform, View } from "react-native"; +import { ActivityIndicator, Platform, View } from "react-native"; import { AppPressable } from "@/components/AppPressable"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import type { EditLocationScreenHandle } from "@/components/screens/EditLocationScreen"; import EditLocationScreenComponent from "@/components/screens/EditLocationScreen"; import { type Booking, CalComAPIService } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; export default function EditLocation() { const { uid } = useLocalSearchParams<{ uid: string }>(); @@ -23,13 +24,13 @@ export default function EditLocation() { CalComAPIService.getBookingByUid(uid) .then(setBooking) .catch(() => { - Alert.alert("Error", "Failed to load booking details"); + showErrorAlert("Error", "Failed to load booking details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/mark-no-show.ios.tsx b/companion/app/mark-no-show.ios.tsx index f20985e2be..c4a383568c 100644 --- a/companion/app/mark-no-show.ios.tsx +++ b/companion/app/mark-no-show.ios.tsx @@ -2,10 +2,11 @@ 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 { ActivityIndicator, 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"; +import { showErrorAlert } from "@/utils/alerts"; interface Attendee { id?: number | string; @@ -57,13 +58,13 @@ export default function MarkNoShowIOS() { setAttendees(bookingAttendees); }) .catch(() => { - Alert.alert("Error", "Failed to load booking details"); + showErrorAlert("Error", "Failed to load booking details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/mark-no-show.tsx b/companion/app/mark-no-show.tsx index 2fc359ce0b..8c4acb9924 100644 --- a/companion/app/mark-no-show.tsx +++ b/companion/app/mark-no-show.tsx @@ -1,11 +1,12 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useState } from "react"; -import { ActivityIndicator, Alert, Platform, View } from "react-native"; +import { ActivityIndicator, Platform, View } from "react-native"; import { AppPressable } from "@/components/AppPressable"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import MarkNoShowScreenComponent from "@/components/screens/MarkNoShowScreen"; import { type Booking, CalComAPIService } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; interface Attendee { id?: number | string; @@ -50,13 +51,13 @@ export default function MarkNoShow() { setAttendees(bookingAttendees); }) .catch(() => { - Alert.alert("Error", "Failed to load booking details"); + showErrorAlert("Error", "Failed to load booking details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/meeting-session-details.ios.tsx b/companion/app/meeting-session-details.ios.tsx index 5a48ff3ef0..2a959e3525 100644 --- a/companion/app/meeting-session-details.ios.tsx +++ b/companion/app/meeting-session-details.ios.tsx @@ -2,11 +2,12 @@ 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 { ActivityIndicator, 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"; +import { showErrorAlert } from "@/utils/alerts"; /** * Get the presentation style for the meeting session details sheet @@ -35,13 +36,13 @@ export default function MeetingSessionDetailsIOS() { setSessions(sessionsData); }) .catch(() => { - Alert.alert("Error", "Failed to load session details"); + showErrorAlert("Error", "Failed to load session details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/meeting-session-details.tsx b/companion/app/meeting-session-details.tsx index 7a0a437b76..94534cfcde 100644 --- a/companion/app/meeting-session-details.tsx +++ b/companion/app/meeting-session-details.tsx @@ -1,12 +1,13 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useState } from "react"; -import { ActivityIndicator, Alert, Platform, View } from "react-native"; +import { ActivityIndicator, Platform, View } from "react-native"; import { AppPressable } from "@/components/AppPressable"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import MeetingSessionDetailsScreenComponent from "@/components/screens/MeetingSessionDetailsScreen"; import { CalComAPIService } from "@/services/calcom"; import type { ConferencingSession } from "@/services/types/bookings.types"; +import { showErrorAlert } from "@/utils/alerts"; export default function MeetingSessionDetails() { const { uid } = useLocalSearchParams<{ uid: string }>(); @@ -22,13 +23,13 @@ export default function MeetingSessionDetails() { setSessions(sessionsData); }) .catch(() => { - Alert.alert("Error", "Failed to load booking details"); + showErrorAlert("Error", "Failed to load booking details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/profile-sheet.ios.tsx b/companion/app/profile-sheet.ios.tsx index 229a0e4fcd..b08381b131 100644 --- a/companion/app/profile-sheet.ios.tsx +++ b/companion/app/profile-sheet.ios.tsx @@ -4,9 +4,10 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import * as Haptics from "expo-haptics"; import { Stack, useRouter } from "expo-router"; -import { ActivityIndicator, Alert, Image, Text, TouchableOpacity, View } from "react-native"; +import { ActivityIndicator, Image, Text, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useUserProfile } from "@/hooks"; +import { showSuccessAlert } from "@/utils/alerts"; import { openInAppBrowser } from "@/utils/browser"; import { getAvatarUrl } from "@/utils/getAvatarUrl"; @@ -82,7 +83,7 @@ export default function ProfileSheet() { if (publicPageUrl) { await Clipboard.setStringAsync(publicPageUrl); await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); - Alert.alert("Copied!", "Public page link copied to clipboard"); + showSuccessAlert("Copied!", "Public page link copied to clipboard"); } }, external: false, diff --git a/companion/app/profile-sheet.tsx b/companion/app/profile-sheet.tsx index 1cece9f945..c2f8426326 100644 --- a/companion/app/profile-sheet.tsx +++ b/companion/app/profile-sheet.tsx @@ -2,18 +2,11 @@ import { Ionicons } from "@expo/vector-icons"; import * as Clipboard from "expo-clipboard"; import * as Haptics from "expo-haptics"; import { Stack, useRouter } from "expo-router"; -import { - ActivityIndicator, - Alert, - Image, - ScrollView, - Text, - TouchableOpacity, - View, -} from "react-native"; +import { ActivityIndicator, Image, ScrollView, Text, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import { useUserProfile } from "@/hooks"; +import { showSuccessAlert } from "@/utils/alerts"; import { openInAppBrowser } from "@/utils/browser"; import { getAvatarUrl } from "@/utils/getAvatarUrl"; @@ -77,7 +70,7 @@ export default function ProfileSheet() { if (publicPageUrl) { await Clipboard.setStringAsync(publicPageUrl); await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); - Alert.alert("Copied!", "Public page link copied to clipboard"); + showSuccessAlert("Copied!", "Public page link copied to clipboard"); } }, external: false, diff --git a/companion/app/reschedule.ios.tsx b/companion/app/reschedule.ios.tsx index 7d2d5bee7d..1c5ccfaa7d 100644 --- a/companion/app/reschedule.ios.tsx +++ b/companion/app/reschedule.ios.tsx @@ -2,11 +2,12 @@ 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 { ActivityIndicator, 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"; +import { showErrorAlert } from "@/utils/alerts"; // Semi-transparent background to prevent black flash while preserving glass effect const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)"; @@ -34,13 +35,13 @@ export default function RescheduleIOS() { CalComAPIService.getBookingByUid(uid) .then(setBooking) .catch(() => { - Alert.alert("Error", "Failed to load booking details"); + showErrorAlert("Error", "Failed to load booking details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/reschedule.tsx b/companion/app/reschedule.tsx index 90a6f79e59..06b61e1868 100644 --- a/companion/app/reschedule.tsx +++ b/companion/app/reschedule.tsx @@ -1,12 +1,13 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Alert, Platform, View } from "react-native"; +import { ActivityIndicator, Platform, View } from "react-native"; import { AppPressable } from "@/components/AppPressable"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import type { RescheduleScreenHandle } from "@/components/screens/RescheduleScreen"; import RescheduleScreenComponent from "@/components/screens/RescheduleScreen"; import { type Booking, CalComAPIService } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; export default function Reschedule() { const { uid } = useLocalSearchParams<{ uid: string }>(); @@ -23,13 +24,13 @@ export default function Reschedule() { CalComAPIService.getBookingByUid(uid) .then(setBooking) .catch(() => { - Alert.alert("Error", "Failed to load booking details"); + showErrorAlert("Error", "Failed to load booking details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/view-recordings.ios.tsx b/companion/app/view-recordings.ios.tsx index 174df23b2c..3cfa6dd43b 100644 --- a/companion/app/view-recordings.ios.tsx +++ b/companion/app/view-recordings.ios.tsx @@ -2,11 +2,12 @@ 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 { ActivityIndicator, 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 { showErrorAlert } from "@/utils/alerts"; import { safeLogError } from "@/utils/safeLogger"; function getPresentationStyle(): "formSheet" | "modal" { @@ -30,13 +31,13 @@ export default function ViewRecordingsIOS() { .then(setRecordings) .catch((error) => { safeLogError("Failed to load recordings:", error); - Alert.alert("Error", "Failed to load recordings"); + showErrorAlert("Error", "Failed to load recordings"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/view-recordings.tsx b/companion/app/view-recordings.tsx index 3fff6bf752..d727cf721f 100644 --- a/companion/app/view-recordings.tsx +++ b/companion/app/view-recordings.tsx @@ -1,12 +1,13 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useState } from "react"; -import { ActivityIndicator, Alert, Platform, View } from "react-native"; +import { ActivityIndicator, Platform, View } from "react-native"; import { AppPressable } from "@/components/AppPressable"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import ViewRecordingsScreenComponent from "@/components/screens/ViewRecordingsScreen"; import { CalComAPIService } from "@/services/calcom"; import type { BookingRecording } from "@/services/types/bookings.types"; +import { showErrorAlert } from "@/utils/alerts"; import { safeLogError } from "@/utils/safeLogger"; export default function ViewRecordings() { @@ -22,13 +23,13 @@ export default function ViewRecordings() { .then(setRecordings) .catch((error) => { safeLogError("Failed to load recordings:", error); - Alert.alert("Error", "Failed to load recordings"); + showErrorAlert("Error", "Failed to load recordings"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/biome.json b/companion/biome.json index 3cd1189706..bb3eafde64 100644 --- a/companion/biome.json +++ b/companion/biome.json @@ -21,7 +21,8 @@ "!**/*.d.ts", "!**/bun.lock", "!**/android", - "!**/ios" + "!**/ios", + "!**/safari-extension" ] }, "formatter": { diff --git a/companion/components/LocationsList.tsx b/companion/components/LocationsList.tsx index 8783d1999f..eb4e04afa8 100644 --- a/companion/components/LocationsList.tsx +++ b/companion/components/LocationsList.tsx @@ -3,19 +3,13 @@ * Reusable component for displaying and managing multiple event type locations */ +import { Button, ContextMenu, Host, HStack, Image } from "@expo/ui/swift-ui"; +import { buttonStyle, frame } from "@expo/ui/swift-ui/modifiers"; import { Ionicons } from "@expo/vector-icons"; +import { isLiquidGlassAvailable } from "expo-glass-effect"; import type React from "react"; import { useState } from "react"; -import { - ActionSheetIOS, - Modal, - Platform, - ScrollView, - Text, - TextInput, - TouchableOpacity, - View, -} from "react-native"; +import { Modal, Platform, ScrollView, Text, TextInput, TouchableOpacity, View } from "react-native"; import type { LocationItem, LocationOptionGroup } from "@/types/locations"; import { createLocationItemFromOption, @@ -40,6 +34,12 @@ interface LocationsListProps { disabled?: boolean; /** Whether locations are loading */ loading?: boolean; + /** Whether to hide the internal add button */ + hideAddButton?: boolean; + /** External control for the add modal */ + showAddModal?: boolean; + /** Callback to change modal visibility */ + onShowAddModalChange?: (visible: boolean) => void; } function isLocationAlreadyAdded(locations: LocationItem[], optionValue: string): boolean { @@ -54,6 +54,94 @@ function isLocationAlreadyAdded(locations: LocationItem[], optionValue: string): }); } +const getSFSymbolForType = (type: string) => { + switch (type) { + case "address": + return "mappin.and.ellipse"; + case "link": + return "link"; + case "phone": + return "phone"; + default: + return "video"; + } +}; + +export const AddLocationTrigger = ({ + isHeader = false, + locationOptions, + locations, + onSelectOption, + disabled = false, + loading = false, + onPressFallback, +}: { + isHeader?: boolean; + locationOptions: LocationOptionGroup[]; + locations: LocationItem[]; + onSelectOption: (value: string, label: string) => void; + disabled?: boolean; + loading?: boolean; + onPressFallback?: () => void; +}) => { + const trigger = isHeader ? ( + + + + ) : ( + + {loading ? ( + Loading options... + ) : ( + <> + + Add Location + + )} + + ); + + if (Platform.OS !== "ios") { + return trigger; + } + + return ( + + + + {locationOptions.flatMap((group) => + group.options + .filter((opt) => !isLocationAlreadyAdded(locations, opt.value)) + .map((option) => ( +