From 1c3c6292df079f90caa158996ee2266ff9c98b0a Mon Sep 17 00:00:00 2001 From: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:12:06 +0530 Subject: [PATCH] refactor: handle unavailable features on iOS and consolidate settings UI (#27081) - Refactor duplicate UI components (SettingsGroup, SettingRow, NavigationRow) into [SettingsUI.tsx] - Centralize showNotAvailableAlert in utils/alerts.ts - Replace in-app browser links with Feature not available alerts for iOS on: - More tab items (Apps, Routing, Workflows, Insights, Support) - Event Type Other tab (Apps, Workflows, Webhooks) - Event Type Advanced tab (Private Links) - Remove View public page from Profile Sheet on iOS - Remove Preview actions from Event Type (List menu and Detail quick actions) on iOS - Add Learn More links to Advanced Tab settings --- .../(event-types)/event-type-detail.tsx | 33 ++- companion/app/(tabs)/(more)/index.ios.tsx | 32 ++- companion/app/profile-sheet.ios.tsx | 10 +- .../event-type-detail/SettingsUI.tsx | 177 ++++++++++++++++ .../event-type-detail/tabs/AdvancedTab.tsx | 191 ++---------------- .../event-type-detail/tabs/BasicsTab.tsx | 141 +------------ .../event-type-detail/tabs/LimitsTab.tsx | 162 ++------------- .../event-type-detail/tabs/RecurringTab.tsx | 97 +-------- .../EventTypeListItem.ios.tsx | 7 - companion/utils/alerts.ts | 11 + 10 files changed, 265 insertions(+), 596 deletions(-) create mode 100644 companion/components/event-type-detail/SettingsUI.tsx diff --git a/companion/app/(tabs)/(event-types)/event-type-detail.tsx b/companion/app/(tabs)/(event-types)/event-type-detail.tsx index 6130b535bd..68e8950ed3 100644 --- a/companion/app/(tabs)/(event-types)/event-type-detail.tsx +++ b/companion/app/(tabs)/(event-types)/event-type-detail.tsx @@ -36,7 +36,12 @@ import { } from "@/services/calcom"; import { useCreateEventType, useDeleteEventType, useUpdateEventType } from "@/hooks"; import type { LocationItem, LocationOptionGroup } from "@/types/locations"; -import { showErrorAlert, showInfoAlert, showSuccessAlert } from "@/utils/alerts"; +import { + showErrorAlert, + showInfoAlert, + showNotAvailableAlert, + showSuccessAlert, +} from "@/utils/alerts"; import { openInAppBrowser } from "@/utils/browser"; import { buildLocationOptions, @@ -2250,6 +2255,8 @@ export default function EventTypeDetail() { "Info", "Save the event type first to configure this setting." ); + } else if (Platform.OS === "ios") { + showNotAvailableAlert(); } else { openInAppBrowser( `https://app.cal.com/event-types/${id}?tabName=apps`, @@ -2283,6 +2290,8 @@ export default function EventTypeDetail() { "Info", "Save the event type first to configure this setting." ); + } else if (Platform.OS === "ios") { + showNotAvailableAlert(); } else { openInAppBrowser( `https://app.cal.com/event-types/${id}?tabName=workflows`, @@ -2316,6 +2325,8 @@ export default function EventTypeDetail() { "Info", "Save the event type first to configure this setting." ); + } else if (Platform.OS === "ios") { + showNotAvailableAlert(); } else { openInAppBrowser( `https://app.cal.com/event-types/${id}?tabName=webhooks`, @@ -2382,15 +2393,17 @@ export default function EventTypeDetail() { - - Preview - - + {Platform.OS !== "ios" && ( + + Preview + + + )} openInAppBrowser("https://app.cal.com/apps", "Apps page"), + isExternal: false, + onPress: () => showNotAvailableAlert(), }, { name: "Routing", icon: "git-branch-outline", - isExternal: true, - onPress: () => openInAppBrowser("https://app.cal.com/routing", "Routing page"), + isExternal: false, + onPress: () => showNotAvailableAlert(), }, { name: "Workflows", icon: "flash-outline", - isExternal: true, - onPress: () => openInAppBrowser("https://app.cal.com/workflows", "Workflows page"), + isExternal: false, + onPress: () => showNotAvailableAlert(), }, { name: "Insights", icon: "bar-chart-outline", - isExternal: true, - onPress: () => openInAppBrowser("https://app.cal.com/insights", "Insights page"), + isExternal: false, + onPress: () => showNotAvailableAlert(), }, { name: "Support", icon: "help-circle-outline", - isExternal: true, - onPress: () => openInAppBrowser("https://go.cal.com/support", "Support"), + isExternal: false, + onPress: () => showNotAvailableAlert(), }, ]; @@ -172,13 +174,7 @@ export default function More() { {/* Footer Note */} The companion app is an extension of the web application.{"\n"} - For advanced features, visit{" "} - openInAppBrowser("https://app.cal.com", "Cal.com")} - > - app.cal.com - + For advanced features, visit app.cal.com diff --git a/companion/app/profile-sheet.ios.tsx b/companion/app/profile-sheet.ios.tsx index 2f85b57529..6ccd9eff9e 100644 --- a/companion/app/profile-sheet.ios.tsx +++ b/companion/app/profile-sheet.ios.tsx @@ -66,15 +66,7 @@ export default function ProfileSheet() { ), external: true, }, - { - id: "publicPage", - label: "View public page", - icon: "globe-outline", - onPress: () => { - if (publicPageUrl) openInAppBrowser(publicPageUrl, "Public page"); - }, - external: true, - }, + { id: "copyPublicPage", label: "Copy public page link", diff --git a/companion/components/event-type-detail/SettingsUI.tsx b/companion/components/event-type-detail/SettingsUI.tsx new file mode 100644 index 0000000000..3660c129b7 --- /dev/null +++ b/companion/components/event-type-detail/SettingsUI.tsx @@ -0,0 +1,177 @@ +import { Ionicons } from "@expo/vector-icons"; +import { Alert, Platform, Switch, Text, TouchableOpacity, View } from "react-native"; +import { openInAppBrowser } from "@/utils/browser"; +import { IOSPickerTrigger } from "./tabs/IOSPickerTrigger"; + +// Section header +export function SectionHeader({ + title, + rightElement, +}: { + title: string; + rightElement?: React.ReactNode; +}) { + return ( + + + {title} + + {rightElement} + + ); +} + +// Settings group container +export function SettingsGroup({ + children, + header, + headerRight, + footer, +}: { + children: React.ReactNode; + header?: string; + headerRight?: React.ReactNode; + footer?: string; +}) { + return ( + + {header ? : null} + {children} + {footer ? {footer} : null} + + ); +} + +// Toggle row with indented separator +export function SettingRow({ + title, + description, + value, + onValueChange, + learnMoreUrl, + isFirst = false, + isLast = false, +}: { + title: string; + description?: string; + value: boolean; + onValueChange: (value: boolean) => void; + learnMoreUrl?: string; + isFirst?: boolean; + isLast?: boolean; +}) { + const height = isFirst || isLast ? 52 : 44; + const showDescription = () => { + if (!description) return; + + const buttons: { + text: string; + onPress?: () => void; + style?: "cancel" | "default" | "destructive"; + }[] = [{ text: "OK", style: "cancel" }]; + + if (learnMoreUrl) { + buttons.unshift({ + text: "Learn more", + onPress: () => openInAppBrowser(learnMoreUrl, "Learn more"), + }); + } + + Alert.alert(title, description, buttons); + }; + + return ( + + + + + {title} + + {description ? ( + + ) : null} + + + + + + + ); +} + +// Navigation row (with chevron) +export function NavigationRow({ + title, + value, + onPress, + isFirst = false, + isLast = false, + options, + onSelect, +}: { + title: string; + value?: string; + onPress: () => void; + isFirst?: boolean; + isLast?: boolean; + options?: { label: string; value: string }[]; + onSelect?: (value: string) => void; +}) { + const height = isFirst || isLast ? 52 : 44; + return ( + + + + {title} + + + {Platform.OS === "ios" && options && onSelect ? ( + <> + {value ? ( + + {value} + + ) : null} + + + ) : ( + + {value ? ( + + {value} + + ) : null} + + + )} + + + + ); +} diff --git a/companion/components/event-type-detail/tabs/AdvancedTab.tsx b/companion/components/event-type-detail/tabs/AdvancedTab.tsx index 3e55b519a8..0c9fb87389 100644 --- a/companion/components/event-type-detail/tabs/AdvancedTab.tsx +++ b/companion/components/event-type-detail/tabs/AdvancedTab.tsx @@ -12,16 +12,15 @@ import { Modal, Platform, ScrollView, - Switch, Text, TextInput, TouchableOpacity, View, } from "react-native"; -import { showInfoAlert } from "@/utils/alerts"; +import { showInfoAlert, showNotAvailableAlert } from "@/utils/alerts"; import { openInAppBrowser } from "@/utils/browser"; -import { IOSPickerTrigger } from "./IOSPickerTrigger"; +import { NavigationRow, SettingRow, SettingsGroup } from "../SettingsUI"; // Interface language options matching API V2 enum const interfaceLanguageOptions = [ @@ -65,167 +64,7 @@ const interfaceLanguageOptions = [ { label: "中文(台灣)", value: "zh-TW" }, ]; -// Section header -function SectionHeader({ title }: { title: string }) { - return ( - - {title} - - ); -} - -// Settings group container -function SettingsGroup({ - children, - header, - footer, -}: { - children: React.ReactNode; - header?: string; - footer?: string; -}) { - return ( - - {header ? : null} - {children} - {footer ? {footer} : null} - - ); -} - -// Toggle row with indented separator -function SettingRow({ - title, - description, - value, - onValueChange, - learnMoreUrl, - isFirst = false, - isLast = false, -}: { - title: string; - description?: string; - value: boolean; - onValueChange: (value: boolean) => void; - learnMoreUrl?: string; - isFirst?: boolean; - isLast?: boolean; -}) { - const height = isFirst || isLast ? 52 : 44; - const showDescription = () => { - if (!description) return; - - const buttons: { - text: string; - onPress?: () => void; - style?: "cancel" | "default" | "destructive"; - }[] = [{ text: "OK", style: "cancel" }]; - - if (learnMoreUrl) { - buttons.unshift({ - text: "Learn more", - onPress: () => openInAppBrowser(learnMoreUrl, "Learn more"), - }); - } - - Alert.alert(title, description, buttons); - }; - - return ( - - - - - {title} - - {description ? ( - - ) : null} - - - - - - - ); -} - -// Navigation row (with chevron) -function NavigationRow({ - title, - value, - onPress, - isFirst = false, - isLast = false, - options, - onSelect, -}: { - title: string; - value?: string; - onPress: () => void; - isFirst?: boolean; - isLast?: boolean; - options?: { label: string; value: string }[]; - onSelect?: (value: string) => void; -}) { - const height = isFirst || isLast ? 52 : 44; - return ( - - - - {title} - - - {Platform.OS === "ios" && options && onSelect ? ( - <> - {value ? ( - - {value} - - ) : null} - - - ) : ( - - {value ? ( - - {value} - - ) : null} - - - )} - - - - ); -} +// Local components removed in favor of SettingsUI interface AdvancedTabProps { requiresConfirmation: boolean; @@ -289,6 +128,8 @@ export function AdvancedTab(props: AdvancedTabProps) { return option?.label || "Browser Default"; }; + // handleNotAvailable replaced by global utility + return ( {/* Booking Confirmation Group */} @@ -297,6 +138,7 @@ export function AdvancedTab(props: AdvancedTabProps) { isFirst title="Requires confirmation" description="The booking needs to be manually confirmed before it is pushed to your calendar and a confirmation is sent." + learnMoreUrl="https://cal.com/help/event-types/how-to-requires" value={props.requiresConfirmation} onValueChange={(value) => { if (value && props.seatsEnabled) { @@ -323,21 +165,24 @@ export function AdvancedTab(props: AdvancedTabProps) { @@ -386,6 +233,7 @@ export function AdvancedTab(props: AdvancedTabProps) { description="Arrange time slots to optimize availability." value={props.showOptimizedSlots} onValueChange={props.setShowOptimizedSlots} + learnMoreUrl="https://cal.com/help/event-types/optimized-slots#optimized-slots" /> @@ -603,10 +452,7 @@ export function AdvancedTab(props: AdvancedTabProps) { title="Private Links" onPress={() => { if (props.eventTypeId && props.eventTypeId !== "new") { - openInAppBrowser( - `https://app.cal.com/event-types/${props.eventTypeId}?tabName=advanced`, - "Private Links" - ); + showNotAvailableAlert(); } else { showInfoAlert("Info", "Save the event type first to configure this setting."); } @@ -616,10 +462,7 @@ export function AdvancedTab(props: AdvancedTabProps) { title="Custom Reply-To Email" onPress={() => { if (props.eventTypeId && props.eventTypeId !== "new") { - openInAppBrowser( - `https://app.cal.com/event-types/${props.eventTypeId}?tabName=advanced`, - "Custom Reply-To" - ); + showNotAvailableAlert(); } else { showInfoAlert("Info", "Save the event type first to configure this setting."); } diff --git a/companion/components/event-type-detail/tabs/BasicsTab.tsx b/companion/components/event-type-detail/tabs/BasicsTab.tsx index 43cab9ec9e..8c904edf6f 100644 --- a/companion/components/event-type-detail/tabs/BasicsTab.tsx +++ b/companion/components/event-type-detail/tabs/BasicsTab.tsx @@ -4,15 +4,14 @@ * iOS Settings style with grouped input rows and section headers. */ -import { Ionicons } from "@expo/vector-icons"; import type React from "react"; import { useState } from "react"; -import { Platform, Switch, Text, TextInput, TouchableOpacity, View } from "react-native"; +import { Text, TextInput, View } from "react-native"; import { AddLocationTrigger, LocationsList } from "@/components/LocationsList"; import type { LocationItem, LocationOptionGroup } from "@/types/locations"; import { createLocationItemFromOption } from "@/utils/locationHelpers"; import { slugify } from "@/utils/slugify"; -import { BasicsTabIOSPicker } from "./BasicsTabIOSPicker"; +import { NavigationRow, SettingRow, SettingsGroup } from "../SettingsUI"; interface BasicsTabProps { eventTitle: string; @@ -40,38 +39,7 @@ interface BasicsTabProps { bookingUrl?: string; } -// Section header -function SectionHeader({ title, rightElement }: { title: string; rightElement?: React.ReactNode }) { - return ( - - - {title} - - {rightElement} - - ); -} - -// Settings group container -function SettingsGroup({ - children, - header, - headerRight, -}: { - children: React.ReactNode; - header?: string; - headerRight?: React.ReactNode; -}) { - return ( - - {header ? : null} - {children} - - ); -} +// Local components removed in favor of SettingsUI // Input row with label function InputRow({ @@ -119,106 +87,7 @@ function InputRow({ ); } -// Navigation row with value and chevron -function NavigationRow({ - title, - value, - onPress, - isFirst = false, - isLast = false, - options, - onSelect, -}: { - title: string; - value?: string; - onPress: () => void; - isFirst?: boolean; - isLast?: boolean; - options?: string[]; - onSelect?: (value: string) => void; -}) { - const height = isFirst || isLast ? 52 : 44; - return ( - - - - {title} - - - {Platform.OS === "ios" && options && onSelect ? ( - <> - {value ? ( - - {value} - - ) : null} - - - ) : ( - - {value ? ( - - {value} - - ) : null} - - - )} - - - - ); -} - -// Toggle row -function SettingRow({ - title, - value, - onValueChange, - isFirst = false, - isLast = false, -}: { - title: string; - value: boolean; - onValueChange: (value: boolean) => void; - isFirst?: boolean; - isLast?: boolean; -}) { - const height = isFirst || isLast ? 52 : 44; - return ( - - - - {title} - - - - - - - ); -} +// NavigationRow and SettingRow removed in favor of SettingsUI export const BasicsTab: React.FC = (props) => { const [showLocationModal, setShowLocationModal] = useState(false); @@ -326,7 +195,7 @@ export const BasicsTab: React.FC = (props) => { title="Default duration" value={props.defaultDuration || "Select"} onPress={() => props.setShowDefaultDurationDropdown(true)} - options={props.selectedDurations} + options={props.selectedDurations.map((d) => ({ label: d, value: d }))} onSelect={props.setDefaultDuration} /> ) : null} diff --git a/companion/components/event-type-detail/tabs/LimitsTab.tsx b/companion/components/event-type-detail/tabs/LimitsTab.tsx index c3f718b9c3..b5c6e80f5f 100644 --- a/companion/components/event-type-detail/tabs/LimitsTab.tsx +++ b/companion/components/event-type-detail/tabs/LimitsTab.tsx @@ -6,17 +6,9 @@ */ import { Ionicons } from "@expo/vector-icons"; -import { - Alert, - type Animated, - Platform, - Switch, - Text, - TextInput, - TouchableOpacity, - View, -} from "react-native"; +import { type Animated, Platform, Text, TextInput, TouchableOpacity, View } from "react-native"; +import { NavigationRow, SettingRow, SettingsGroup } from "../SettingsUI"; import { LimitsTabDatePicker } from "./LimitsTabDatePicker"; import { LimitsTabIOSPicker } from "./LimitsTabIOSPicker"; @@ -71,88 +63,7 @@ const FREQUENCY_UNIT_OPTIONS = ["Per day", "Per week", "Per month", "Per year"]; // Duration unit options const DURATION_UNIT_OPTIONS = ["Per day", "Per week", "Per month"]; -// Section header -function SectionHeader({ title }: { title: string }) { - return ( - - {title} - - ); -} - -// Settings group container -function SettingsGroup({ - children, - header, - footer, -}: { - children: React.ReactNode; - header?: string; - footer?: string; -}) { - return ( - - {header ? : null} - {children} - {footer ? {footer} : null} - - ); -} - -// Toggle row with indented separator -function SettingRow({ - title, - description, - value, - onValueChange, - isLast = false, -}: { - title: string; - description?: string; - value: boolean; - onValueChange: (value: boolean) => void; - isLast?: boolean; -}) { - const showDescription = () => { - if (!description) return; - Alert.alert(title, description, [{ text: "OK", style: "cancel" }]); - }; - - return ( - - - - - {title} - - {description ? ( - - ) : null} - - - - - - - ); -} +// Local components removed in favor of SettingsUI // Android/Web picker button (opens modal) function ModalPickerButton({ value, onPress }: { value: string; onPress: () => void }) { @@ -168,55 +79,6 @@ function ModalPickerButton({ value, onPress }: { value: string; onPress: () => v } // Navigation row with native iOS picker or modal button -function PickerNavigationRow({ - title, - value, - options, - onSelect, - onPressModal, - isLast = false, -}: { - title: string; - value: string; - options: string[]; - onSelect: (value: string) => void; - onPressModal: () => void; - isLast?: boolean; -}) { - return ( - - - - {title} - - - {Platform.OS === "ios" ? ( - <> - {value} - - - ) : ( - - - {value} - - - - )} - - - - ); -} interface LimitsTabProps { beforeEventBuffer: string; @@ -276,19 +138,19 @@ export function LimitsTab(props: LimitsTabProps) { {/* Buffer & Notice Settings */} - ({ label: o, value: o }))} onSelect={props.setBeforeEventBuffer} - onPressModal={() => props.setShowBeforeBufferDropdown(true)} + onPress={() => props.setShowBeforeBufferDropdown(true)} /> - ({ label: o, value: o }))} onSelect={props.setAfterEventBuffer} - onPressModal={() => props.setShowAfterBufferDropdown(true)} + onPress={() => props.setShowAfterBufferDropdown(true)} /> - ({ label: o, value: o }))} onSelect={props.setSlotInterval} - onPressModal={() => props.setShowSlotIntervalDropdown(true)} + onPress={() => props.setShowSlotIntervalDropdown(true)} isLast /> diff --git a/companion/components/event-type-detail/tabs/RecurringTab.tsx b/companion/components/event-type-detail/tabs/RecurringTab.tsx index e4be9e26b2..ce0be95264 100644 --- a/companion/components/event-type-detail/tabs/RecurringTab.tsx +++ b/companion/components/event-type-detail/tabs/RecurringTab.tsx @@ -4,10 +4,10 @@ * iOS Settings style with grouped rows and section headers. */ -import { Ionicons } from "@expo/vector-icons"; -import { Alert, Platform, Switch, Text, TextInput, TouchableOpacity, View } from "react-native"; -import { openInAppBrowser } from "@/utils/browser"; +import { Platform, Text, TextInput, TouchableOpacity, View } from "react-native"; +import { SettingRow, SettingsGroup } from "../SettingsUI"; import { RecurringTabIOSPicker } from "./RecurringTabIOSPicker"; +import { Ionicons } from "@expo/vector-icons"; interface RecurringTabProps { recurringEnabled: boolean; @@ -28,94 +28,7 @@ const frequencyToLabel: Record = { yearly: "year", }; -// Section header -function SectionHeader({ title }: { title: string }) { - return ( - - {title} - - ); -} - -// Settings group container -function SettingsGroup({ children, header }: { children: React.ReactNode; header?: string }) { - return ( - - {header ? : null} - {children} - - ); -} - -// Toggle row with description -function SettingRow({ - title, - description, - value, - onValueChange, - learnMoreUrl, - isLast = false, -}: { - title: string; - description?: string; - value: boolean; - onValueChange: (value: boolean) => void; - learnMoreUrl?: string; - isLast?: boolean; -}) { - const showDescription = () => { - if (!description) return; - - const buttons: { - text: string; - onPress?: () => void; - style?: "cancel" | "default" | "destructive"; - }[] = [{ text: "OK", style: "cancel" }]; - - if (learnMoreUrl) { - buttons.unshift({ - text: "Learn more", - onPress: () => openInAppBrowser(learnMoreUrl, "Learn more"), - }); - } - - Alert.alert(title, description, buttons); - }; - - return ( - - - - - {title} - - {description ? ( - - ) : null} - - - - - - - ); -} +// Local components removed in favor of SettingsUI export function RecurringTab({ recurringEnabled, @@ -137,7 +50,7 @@ export function RecurringTab({ description="People can subscribe for recurring events. When enabled, you can set how often the event repeats and for how many occurrences." value={recurringEnabled} onValueChange={setRecurringEnabled} - learnMoreUrl="https://cal.com/docs/core-features/event-types/recurring-events" + learnMoreUrl="https://cal.com/help/event-types/recurring-events" isLast /> diff --git a/companion/components/event-type-list-item/EventTypeListItem.ios.tsx b/companion/components/event-type-list-item/EventTypeListItem.ios.tsx index 5e02f3bce1..3623084d0d 100644 --- a/companion/components/event-type-list-item/EventTypeListItem.ios.tsx +++ b/companion/components/event-type-list-item/EventTypeListItem.ios.tsx @@ -13,7 +13,6 @@ export const EventTypeListItem = ({ filteredEventTypes, handleEventTypePress, handleCopyLink, - handlePreview, onEdit, onDuplicate, onDelete, @@ -59,12 +58,6 @@ export const EventTypeListItem = ({ onPress: () => void; role: "default" | "destructive"; }[] = [ - { - label: "Preview", - icon: "arrow.up.right.square", - onPress: () => handlePreview(item), - role: "default", - }, { label: "Copy link", icon: "link", diff --git a/companion/utils/alerts.ts b/companion/utils/alerts.ts index 4fd562bbf2..b7f6e1f9b8 100644 --- a/companion/utils/alerts.ts +++ b/companion/utils/alerts.ts @@ -88,3 +88,14 @@ export const showInfoAlert = (title: string, message: string) => { Alert.alert(title, message); }; + +/** + * Show a standard "Feature not available" alert for companion app limitations. + */ +export const showNotAvailableAlert = () => { + Alert.alert( + "Feature not available", + "This feature is not available on app yet. To use, please visit app.cal.com.", + [{ text: "OK", style: "cancel" }] + ); +};