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
This commit is contained in:
Dhairyashil Shinde
2026-01-21 09:42:06 -03:00
committed by GitHub
parent cb567a91c5
commit 1c3c6292df
10 changed files with 265 additions and 596 deletions
@@ -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() {
</Text>
<View className="overflow-hidden rounded-[10px] bg-white">
<View className="bg-white pl-4">
<TouchableOpacity
className="flex-row items-center justify-between border-b border-[#E5E5E5] pr-4"
style={{ height: 44 }}
onPress={handlePreview}
activeOpacity={0.5}
>
<Text className="text-[17px] text-black">Preview</Text>
<Ionicons name="open-outline" size={18} color="#C7C7CC" />
</TouchableOpacity>
{Platform.OS !== "ios" && (
<TouchableOpacity
className="flex-row items-center justify-between border-b border-[#E5E5E5] pr-4"
style={{ height: 44 }}
onPress={handlePreview}
activeOpacity={0.5}
>
<Text className="text-[17px] text-black">Preview</Text>
<Ionicons name="open-outline" size={18} color="#C7C7CC" />
</TouchableOpacity>
)}
</View>
<View className="bg-white pl-4">
<TouchableOpacity
+14 -18
View File
@@ -8,7 +8,7 @@ import { LogoutConfirmModal } from "@/components/LogoutConfirmModal";
import { useAuth } from "@/contexts/AuthContext";
import { useQueryContext } from "@/contexts/QueryContext";
import { useUserProfile } from "@/hooks";
import { showErrorAlert } from "@/utils/alerts";
import { showErrorAlert, showNotAvailableAlert } from "@/utils/alerts";
import { openInAppBrowser } from "@/utils/browser";
import { getAvatarUrl } from "@/utils/getAvatarUrl";
@@ -54,36 +54,38 @@ export default function More() {
]);
};
// handleNotAvailable replaced by global showNotAvailableAlert
const menuItems: MoreMenuItem[] = [
{
name: "Apps",
icon: "grid-outline",
isExternal: true,
onPress: () => 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 */}
<Text className="mt-6 px-1 text-center text-xs text-gray-400">
The companion app is an extension of the web application.{"\n"}
For advanced features, visit{" "}
<Text
className="text-gray-800"
onPress={() => openInAppBrowser("https://app.cal.com", "Cal.com")}
>
app.cal.com
</Text>
For advanced features, visit <Text className="text-gray-800">app.cal.com</Text>
</Text>
</ScrollView>
+1 -9
View File
@@ -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",
@@ -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 (
<View className={`flex-row items-center ${rightElement ? "justify-between pr-4" : ""} mb-2`}>
<Text
className="ml-4 text-[13px] uppercase tracking-wide text-[#6D6D72]"
style={{ letterSpacing: 0.5 }}
>
{title}
</Text>
{rightElement}
</View>
);
}
// Settings group container
export function SettingsGroup({
children,
header,
headerRight,
footer,
}: {
children: React.ReactNode;
header?: string;
headerRight?: React.ReactNode;
footer?: string;
}) {
return (
<View>
{header ? <SectionHeader title={header} rightElement={headerRight} /> : null}
<View className="overflow-hidden rounded-[14px] bg-white">{children}</View>
{footer ? <Text className="ml-4 mt-2 text-[13px] text-[#6D6D72]">{footer}</Text> : null}
</View>
);
}
// 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 (
<View className="bg-white pl-4">
<View
className={`flex-row items-center pr-4 ${!isLast ? "border-b border-[#E5E5E5]" : ""}`}
style={{ height, flexDirection: "row", alignItems: "center" }}
>
<TouchableOpacity
className="flex-1 flex-row items-center"
style={{ height }}
onPress={description ? showDescription : undefined}
activeOpacity={description ? 0.7 : 1}
disabled={!description}
>
<Text className="text-[17px] text-black" style={{ fontWeight: "400" }}>
{title}
</Text>
{description ? (
<Ionicons name="chevron-down" size={12} color="#C7C7CC" style={{ marginLeft: 6 }} />
) : null}
</TouchableOpacity>
<View style={{ alignSelf: "center", justifyContent: "center" }}>
<Switch
value={value}
onValueChange={onValueChange}
trackColor={{ false: "#E9E9EA", true: "#000000" }}
thumbColor={Platform.OS !== "ios" ? "#FFFFFF" : undefined}
/>
</View>
</View>
</View>
);
}
// 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 (
<View className="bg-white pl-4" style={{ height }}>
<View
className={`flex-1 flex-row items-center justify-between pr-4 ${
!isLast ? "border-b border-[#E5E5E5]" : ""
}`}
style={{ height }}
>
<Text className="text-[17px] text-black" style={{ fontWeight: "400" }}>
{title}
</Text>
<View className="flex-row items-center">
{Platform.OS === "ios" && options && onSelect ? (
<>
{value ? (
<Text className="mr-2 text-[17px] text-[#8E8E93]" numberOfLines={1}>
{value}
</Text>
) : null}
<IOSPickerTrigger options={options} selectedValue={value || ""} onSelect={onSelect} />
</>
) : (
<TouchableOpacity
className="flex-row items-center"
onPress={onPress}
activeOpacity={0.5}
>
{value ? (
<Text className="mr-1 text-[17px] text-[#8E8E93]" numberOfLines={1}>
{value}
</Text>
) : null}
<Ionicons name="chevron-forward" size={20} color="#C7C7CC" />
</TouchableOpacity>
)}
</View>
</View>
</View>
);
}
@@ -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 (
<Text
className="mb-2 ml-4 text-[13px] uppercase tracking-wide text-[#6D6D72]"
style={{ letterSpacing: 0.5 }}
>
{title}
</Text>
);
}
// Settings group container
function SettingsGroup({
children,
header,
footer,
}: {
children: React.ReactNode;
header?: string;
footer?: string;
}) {
return (
<View>
{header ? <SectionHeader title={header} /> : null}
<View className="overflow-hidden rounded-[14px] bg-white">{children}</View>
{footer ? <Text className="ml-4 mt-2 text-[13px] text-[#6D6D72]">{footer}</Text> : null}
</View>
);
}
// 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 (
<View className="bg-white pl-4">
<View
className={`flex-row items-center pr-4 ${!isLast ? "border-b border-[#E5E5E5]" : ""}`}
style={{ height, flexDirection: "row", alignItems: "center" }}
>
<TouchableOpacity
className="flex-1 flex-row items-center"
style={{ height }}
onPress={description ? showDescription : undefined}
activeOpacity={description ? 0.7 : 1}
disabled={!description}
>
<Text className="text-[17px] text-black" style={{ fontWeight: "400" }}>
{title}
</Text>
{description ? (
<Ionicons name="chevron-down" size={12} color="#C7C7CC" style={{ marginLeft: 6 }} />
) : null}
</TouchableOpacity>
<View style={{ alignSelf: "center", justifyContent: "center" }}>
<Switch
value={value}
onValueChange={onValueChange}
trackColor={{ false: "#E9E9EA", true: "#000000" }}
thumbColor={Platform.OS !== "ios" ? "#FFFFFF" : undefined}
/>
</View>
</View>
</View>
);
}
// 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 (
<View className="bg-white pl-4" style={{ height }}>
<View
className={`flex-1 flex-row items-center justify-between pr-4 ${
!isLast ? "border-b border-[#E5E5E5]" : ""
}`}
style={{ height }}
>
<Text className="text-[17px] text-black" style={{ fontWeight: "400" }}>
{title}
</Text>
<View className="flex-row items-center">
{Platform.OS === "ios" && options && onSelect ? (
<>
{value ? (
<Text className="mr-2 text-[17px] text-[#8E8E93]" numberOfLines={1}>
{value}
</Text>
) : null}
<IOSPickerTrigger options={options} selectedValue={value || ""} onSelect={onSelect} />
</>
) : (
<TouchableOpacity
className="flex-row items-center"
onPress={onPress}
activeOpacity={0.5}
>
{value ? (
<Text className="mr-1 text-[17px] text-[#8E8E93]" numberOfLines={1}>
{value}
</Text>
) : null}
<Ionicons name="chevron-forward" size={20} color="#C7C7CC" />
</TouchableOpacity>
)}
</View>
</View>
</View>
);
}
// 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 (
<View className="gap-6">
{/* 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) {
<SettingRow
isFirst
title="Disable Cancelling"
description="Guests and Organizer can no longer cancel the event with calendar invite or email."
description="Guests and organizer can no longer cancel the event with calendar invite or email."
value={props.disableCancelling}
onValueChange={props.setDisableCancelling}
learnMoreUrl="https://cal.com/help/event-types/disable-canceling-rescheduling#disable-cancelling"
/>
<SettingRow
title="Disable Rescheduling"
description="Guests and Organizer can no longer reschedule the event with calendar invite or email."
value={props.disableRescheduling}
onValueChange={props.setDisableRescheduling}
learnMoreUrl="https://cal.com/help/event-types/disable-canceling-rescheduling#disable-rescheduling"
/>
<SettingRow
title="Reschedule past events"
description="Enabling this option allows for past events to be rescheduled."
value={props.allowReschedulingPastEvents}
onValueChange={props.setAllowReschedulingPastEvents}
learnMoreUrl="https://cal.com/help/event-types/allow-rescheduling"
/>
<SettingRow
title="Book via reschedule link"
@@ -356,6 +201,7 @@ export function AdvancedTab(props: AdvancedTabProps) {
description="For privacy reasons, additional inputs and notes will be hidden in the calendar entry. They will still be sent to your email."
value={props.hideCalendarNotes}
onValueChange={props.setHideCalendarNotes}
learnMoreUrl="https://cal.com/help/event-types/hide-notes"
/>
<SettingRow
title="Hide calendar event details"
@@ -368,6 +214,7 @@ export function AdvancedTab(props: AdvancedTabProps) {
description="Hide organizer's email address from the booking screen, email notifications, and calendar events."
value={props.hideOrganizerEmail}
onValueChange={props.setHideOrganizerEmail}
learnMoreUrl="https://cal.com/help/event-types/hideorganizersemail#hide-organizers-email"
isLast
/>
</SettingsGroup>
@@ -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"
/>
<SettingRow
title="Lock timezone"
@@ -509,7 +357,8 @@ export function AdvancedTab(props: AdvancedTabProps) {
backgroundColor: "#F2F2F7",
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
maxHeight: "70%",
height: "70%",
width: "100%",
}
}
>
@@ -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.");
}
@@ -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 (
<View className="flex-row items-center justify-between mb-2 px-4">
<Text
className="text-[13px] uppercase tracking-wide text-[#6D6D72]"
style={{ letterSpacing: 0.5 }}
>
{title}
</Text>
{rightElement}
</View>
);
}
// Settings group container
function SettingsGroup({
children,
header,
headerRight,
}: {
children: React.ReactNode;
header?: string;
headerRight?: React.ReactNode;
}) {
return (
<View>
{header ? <SectionHeader title={header} rightElement={headerRight} /> : null}
<View className="overflow-hidden rounded-[14px] bg-white">{children}</View>
</View>
);
}
// 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 (
<View className="bg-white pl-4" style={{ height }}>
<View
className={`flex-1 flex-row items-center justify-between pr-4 ${
!isLast ? "border-b border-[#E5E5E5]" : ""
}`}
style={{ height }}
>
<Text className="text-[17px] text-black" style={{ fontWeight: "400" }}>
{title}
</Text>
<View className="flex-row items-center">
{Platform.OS === "ios" && options && onSelect ? (
<>
{value ? (
<Text className="mr-2 text-[17px] text-[#8E8E93]" numberOfLines={1}>
{value}
</Text>
) : null}
<BasicsTabIOSPicker
options={options}
selectedValue={value || ""}
onSelect={onSelect}
/>
</>
) : (
<TouchableOpacity
className="flex-row items-center"
onPress={onPress}
activeOpacity={0.5}
>
{value ? (
<Text className="mr-1 text-[17px] text-[#8E8E93]" numberOfLines={1}>
{value}
</Text>
) : null}
<Ionicons name="chevron-forward" size={20} color="#C7C7CC" />
</TouchableOpacity>
)}
</View>
</View>
</View>
);
}
// 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 (
<View className="bg-white pl-4">
<View
className={`flex-row items-center pr-4 ${!isLast ? "border-b border-[#E5E5E5]" : ""}`}
style={{ height, flexDirection: "row", alignItems: "center" }}
>
<Text className="flex-1 text-[17px] text-black" style={{ fontWeight: "400" }}>
{title}
</Text>
<View style={{ alignSelf: "center", justifyContent: "center" }}>
<Switch
value={value}
onValueChange={onValueChange}
trackColor={{ false: "#E9E9EA", true: "#000000" }}
thumbColor={Platform.OS !== "ios" ? "#FFFFFF" : undefined}
/>
</View>
</View>
</View>
);
}
// NavigationRow and SettingRow removed in favor of SettingsUI
export const BasicsTab: React.FC<BasicsTabProps> = (props) => {
const [showLocationModal, setShowLocationModal] = useState(false);
@@ -326,7 +195,7 @@ export const BasicsTab: React.FC<BasicsTabProps> = (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}
@@ -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 (
<Text
className="mb-2 ml-4 text-[13px] uppercase tracking-wide text-[#6D6D72]"
style={{ letterSpacing: 0.5 }}
>
{title}
</Text>
);
}
// Settings group container
function SettingsGroup({
children,
header,
footer,
}: {
children: React.ReactNode;
header?: string;
footer?: string;
}) {
return (
<View>
{header ? <SectionHeader title={header} /> : null}
<View className="overflow-hidden rounded-[14px] bg-white">{children}</View>
{footer ? <Text className="ml-4 mt-2 text-[13px] text-[#6D6D72]">{footer}</Text> : null}
</View>
);
}
// 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 (
<View className="bg-white pl-4">
<View
className={`flex-row items-center pr-4 ${!isLast ? "border-b border-[#E5E5E5]" : ""}`}
style={{ height: 44, flexDirection: "row", alignItems: "center" }}
>
<TouchableOpacity
className="flex-1 flex-row items-center"
style={{ height: 44 }}
onPress={description ? showDescription : undefined}
activeOpacity={description ? 0.7 : 1}
disabled={!description}
>
<Text className="text-[17px] text-black" style={{ fontWeight: "400" }}>
{title}
</Text>
{description ? (
<Ionicons name="chevron-down" size={12} color="#C7C7CC" style={{ marginLeft: 6 }} />
) : null}
</TouchableOpacity>
<View style={{ alignSelf: "center", justifyContent: "center" }}>
<Switch
value={value}
onValueChange={onValueChange}
trackColor={{ false: "#E9E9EA", true: "#000000" }}
thumbColor={Platform.OS !== "ios" ? "#FFFFFF" : undefined}
/>
</View>
</View>
</View>
);
}
// 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 (
<View className="bg-white pl-4" style={{ height: 44 }}>
<View
className={`flex-1 flex-row items-center justify-between pr-4 ${
!isLast ? "border-b border-[#E5E5E5]" : ""
}`}
style={{ height: 44 }}
>
<Text className="text-[17px] text-black" style={{ fontWeight: "400" }}>
{title}
</Text>
<View className="flex-row items-center">
{Platform.OS === "ios" ? (
<>
<Text className="mr-2 text-[17px] text-[#8E8E93]">{value}</Text>
<LimitsTabIOSPicker options={options} selectedValue={value} onSelect={onSelect} />
</>
) : (
<TouchableOpacity
className="flex-row items-center"
onPress={onPressModal}
activeOpacity={0.5}
>
<Text className="mr-1 text-[17px] text-[#8E8E93]" numberOfLines={1}>
{value}
</Text>
<Ionicons name="chevron-forward" size={20} color="#C7C7CC" />
</TouchableOpacity>
)}
</View>
</View>
</View>
);
}
interface LimitsTabProps {
beforeEventBuffer: string;
@@ -276,19 +138,19 @@ export function LimitsTab(props: LimitsTabProps) {
<View className="gap-6">
{/* Buffer & Notice Settings */}
<SettingsGroup header="Buffer & Notice">
<PickerNavigationRow
<NavigationRow
title="Before event"
value={props.beforeEventBuffer}
options={BUFFER_TIME_OPTIONS}
options={BUFFER_TIME_OPTIONS.map((o) => ({ label: o, value: o }))}
onSelect={props.setBeforeEventBuffer}
onPressModal={() => props.setShowBeforeBufferDropdown(true)}
onPress={() => props.setShowBeforeBufferDropdown(true)}
/>
<PickerNavigationRow
<NavigationRow
title="After event"
value={props.afterEventBuffer}
options={BUFFER_TIME_OPTIONS}
options={BUFFER_TIME_OPTIONS.map((o) => ({ label: o, value: o }))}
onSelect={props.setAfterEventBuffer}
onPressModal={() => props.setShowAfterBufferDropdown(true)}
onPress={() => props.setShowAfterBufferDropdown(true)}
/>
<View className="bg-white pl-4">
<View
@@ -326,12 +188,12 @@ export function LimitsTab(props: LimitsTabProps) {
</View>
</View>
</View>
<PickerNavigationRow
<NavigationRow
title="Time-slot intervals"
value={props.slotInterval === "Default" ? "Event length" : props.slotInterval}
options={SLOT_INTERVAL_OPTIONS}
options={SLOT_INTERVAL_OPTIONS.map((o) => ({ label: o, value: o }))}
onSelect={props.setSlotInterval}
onPressModal={() => props.setShowSlotIntervalDropdown(true)}
onPress={() => props.setShowSlotIntervalDropdown(true)}
isLast
/>
</SettingsGroup>
@@ -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<string, string> = {
yearly: "year",
};
// Section header
function SectionHeader({ title }: { title: string }) {
return (
<Text
className="mb-2 ml-4 text-[13px] uppercase tracking-wide text-[#6D6D72]"
style={{ letterSpacing: 0.5 }}
>
{title}
</Text>
);
}
// Settings group container
function SettingsGroup({ children, header }: { children: React.ReactNode; header?: string }) {
return (
<View>
{header ? <SectionHeader title={header} /> : null}
<View className="overflow-hidden rounded-[14px] bg-white">{children}</View>
</View>
);
}
// 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 (
<View className="bg-white pl-4">
<View
className={`flex-row items-center pr-4 ${!isLast ? "border-b border-[#E5E5E5]" : ""}`}
style={{ minHeight: 44, flexDirection: "row", alignItems: "center" }}
>
<TouchableOpacity
className="flex-1 flex-row items-center py-3"
onPress={description ? showDescription : undefined}
activeOpacity={description ? 0.7 : 1}
disabled={!description}
>
<Text className="text-[17px] text-black" style={{ fontWeight: "400" }}>
{title}
</Text>
{description ? (
<Ionicons name="chevron-down" size={12} color="#C7C7CC" style={{ marginLeft: 6 }} />
) : null}
</TouchableOpacity>
<View style={{ alignSelf: "center", justifyContent: "center" }}>
<Switch
value={value}
onValueChange={onValueChange}
trackColor={{ false: "#E9E9EA", true: "#000000" }}
thumbColor={Platform.OS !== "ios" ? "#FFFFFF" : undefined}
/>
</View>
</View>
</View>
);
}
// 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
/>
</SettingsGroup>
@@ -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",
+11
View File
@@ -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" }]
);
};