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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" }]
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user