mirror of
https://github.com/hanzoai/calendar.git
synced 2026-10-30 14:46:43 +00:00
* feat(companion): add configurable landing page feature - Add useUserPreferences hook for persistent storage of landing page preference - Add LandingPagePicker component for both iOS and Android/web platforms - Update tabs index to redirect based on user preference - Update bookings index to accept initial filter from URL params - Add App Settings section in More screen with landing page selector - Clear user preferences on logout for fresh state * fix(companion): remove try-finally for React Compiler compatibility * fix(companion): use router.replace for dynamic landing page redirect Replace Redirect component with router.replace() to fix TypeScript strict typing issue with expo-router's Href type for dynamic routes. * fix(companion): use literal route strings for TypeScript strict typing Use switch statement with literal route strings instead of dynamic string variable to satisfy expo-router's strict Href type checking. * working fix * better css
363 lines
12 KiB
TypeScript
363 lines
12 KiB
TypeScript
import { Ionicons } from "@expo/vector-icons";
|
|
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
|
import { Image } from "expo-image";
|
|
import { Stack, useRouter } from "expo-router";
|
|
import { useState } from "react";
|
|
import {
|
|
Alert,
|
|
Pressable,
|
|
ScrollView,
|
|
Text,
|
|
TouchableOpacity,
|
|
useColorScheme,
|
|
View,
|
|
} from "react-native";
|
|
import { LandingPagePicker } from "@/components/LandingPagePicker";
|
|
import { LogoutConfirmModal } from "@/components/LogoutConfirmModal";
|
|
import { useAuth } from "@/contexts/AuthContext";
|
|
import { useQueryContext } from "@/contexts/QueryContext";
|
|
import { useUserProfile } from "@/hooks";
|
|
import { type LandingPage, useUserPreferences } from "@/hooks/useUserPreferences";
|
|
import { showErrorAlert, showSuccessAlert, showNotAvailableAlert } from "@/utils/alerts";
|
|
import { openInAppBrowser } from "@/utils/browser";
|
|
import { getAvatarUrl } from "@/utils/getAvatarUrl";
|
|
import { getColors } from "@/constants/colors";
|
|
|
|
interface MoreMenuItem {
|
|
name: string;
|
|
icon: keyof typeof Ionicons.glyphMap;
|
|
isExternal?: boolean;
|
|
href?: string;
|
|
onPress?: () => void;
|
|
}
|
|
|
|
export default function More() {
|
|
const router = useRouter();
|
|
const { logout } = useAuth();
|
|
const { clearCache } = useQueryContext();
|
|
const { preferences, setLandingPage, landingPageLabel } = useUserPreferences();
|
|
const [showLogoutModal, setShowLogoutModal] = useState(false);
|
|
const [showLandingPagePicker, setShowLandingPagePicker] = useState(false);
|
|
const { data: userProfile } = useUserProfile();
|
|
const colorScheme = useColorScheme();
|
|
const isDark = colorScheme === "dark";
|
|
const theme = getColors(isDark);
|
|
|
|
const colors = {
|
|
background: isDark ? "#000000" : "#FFFFFF",
|
|
backgroundSecondary: isDark ? "#171717" : "#FFFFFF",
|
|
backgroundActive: isDark ? "#2C2C2E" : "#F8F9FA",
|
|
text: isDark ? "#FFFFFF" : "#333333",
|
|
textSecondary: isDark ? "#A3A3A3" : "#C7C7CC",
|
|
border: isDark ? "#4D4D4D" : "#E5E5EA",
|
|
};
|
|
|
|
const handleLandingPageSelect = async (value: LandingPage) => {
|
|
try {
|
|
await setLandingPage(value);
|
|
showSuccessAlert("Saved", "First page updated");
|
|
} catch {
|
|
showErrorAlert("Error", "Failed to save preference. Please try again.");
|
|
}
|
|
};
|
|
|
|
const performLogout = async () => {
|
|
try {
|
|
// Clear in-memory cache before logout
|
|
await clearCache();
|
|
await logout();
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
console.error("Logout error", message);
|
|
if (__DEV__) {
|
|
const stack = error instanceof Error ? error.stack : undefined;
|
|
console.debug("[More] logout failed", { message, stack });
|
|
}
|
|
showErrorAlert("Error", "Failed to sign out. Please try again.");
|
|
}
|
|
};
|
|
|
|
const handleSignOut = () => {
|
|
Alert.alert("Sign Out", "Are you sure you want to sign out?", [
|
|
{ text: "Cancel", style: "cancel" },
|
|
{
|
|
text: "Sign Out",
|
|
style: "destructive",
|
|
onPress: performLogout,
|
|
},
|
|
]);
|
|
};
|
|
|
|
// handleNotAvailable replaced by global showNotAvailableAlert
|
|
|
|
const menuItems: MoreMenuItem[] = [
|
|
{
|
|
name: "Apps",
|
|
icon: "grid-outline",
|
|
isExternal: false,
|
|
onPress: () => showNotAvailableAlert(),
|
|
},
|
|
{
|
|
name: "Routing",
|
|
icon: "git-branch-outline",
|
|
isExternal: false,
|
|
onPress: () => showNotAvailableAlert(),
|
|
},
|
|
{
|
|
name: "Workflows",
|
|
icon: "flash-outline",
|
|
isExternal: false,
|
|
onPress: () => showNotAvailableAlert(),
|
|
},
|
|
{
|
|
name: "Insights",
|
|
icon: "bar-chart-outline",
|
|
isExternal: false,
|
|
onPress: () => showNotAvailableAlert(),
|
|
},
|
|
{
|
|
name: "Support",
|
|
icon: "help-circle-outline",
|
|
isExternal: false,
|
|
onPress: () => showNotAvailableAlert(),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<>
|
|
{/* iOS Native Header with Glass UI */}
|
|
<Stack.Header
|
|
style={{ backgroundColor: "transparent", shadowColor: "transparent" }}
|
|
blurEffect={isLiquidGlassAvailable() ? undefined : isDark ? "dark" : "light"}
|
|
>
|
|
<Stack.Header.Title large>More</Stack.Header.Title>
|
|
<Stack.Header.Right>
|
|
{userProfile?.avatarUrl ? (
|
|
<Stack.Header.View>
|
|
<Pressable onPress={() => router.push("/profile-sheet")}>
|
|
<Image
|
|
source={{ uri: getAvatarUrl(userProfile.avatarUrl) }}
|
|
style={{ width: 32, height: 32, borderRadius: 16 }}
|
|
/>
|
|
</Pressable>
|
|
</Stack.Header.View>
|
|
) : (
|
|
<Stack.Header.Button onPress={() => router.push("/profile-sheet")}>
|
|
<Stack.Header.Icon sf="person.circle.fill" />
|
|
</Stack.Header.Button>
|
|
)}
|
|
</Stack.Header.Right>
|
|
</Stack.Header>
|
|
|
|
{/* Content */}
|
|
<ScrollView
|
|
style={{ backgroundColor: colors.background }}
|
|
contentContainerStyle={{ padding: 16, paddingBottom: 120 }}
|
|
showsVerticalScrollIndicator={false}
|
|
contentInsetAdjustmentBehavior="automatic"
|
|
>
|
|
<View
|
|
style={{
|
|
borderRadius: 8,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
backgroundColor: colors.backgroundSecondary,
|
|
overflow: "hidden",
|
|
}}
|
|
>
|
|
{menuItems.map((item, index) => (
|
|
<TouchableOpacity
|
|
key={item.name}
|
|
onPress={item.onPress}
|
|
style={{
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
backgroundColor: colors.backgroundSecondary,
|
|
paddingHorizontal: 20,
|
|
paddingVertical: 20,
|
|
borderBottomWidth: index < menuItems.length - 1 ? 1 : 0,
|
|
borderBottomColor: colors.border,
|
|
}}
|
|
activeOpacity={0.7}
|
|
>
|
|
<View className="flex-1 flex-row items-center">
|
|
<Ionicons name={item.icon} size={20} color={colors.text} />
|
|
<Text style={{ color: colors.text }} className="ml-3 text-base font-semibold">
|
|
{item.name}
|
|
</Text>
|
|
</View>
|
|
{item.isExternal ? (
|
|
<Ionicons name="open-outline" size={20} color={colors.textSecondary} />
|
|
) : (
|
|
<Ionicons name="chevron-forward" size={20} color={colors.textSecondary} />
|
|
)}
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
|
|
{/* App Settings */}
|
|
<View
|
|
style={{
|
|
marginTop: 24,
|
|
borderRadius: 8,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
backgroundColor: colors.backgroundSecondary,
|
|
overflow: "hidden",
|
|
}}
|
|
>
|
|
<View
|
|
style={{
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: colors.border,
|
|
backgroundColor: isDark ? "#2C2C2E" : "#F9FAFB",
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 8,
|
|
}}
|
|
>
|
|
<Text
|
|
style={{ color: isDark ? "#A3A3A3" : "#6B7280" }}
|
|
className="text-xs font-semibold uppercase"
|
|
>
|
|
App Settings
|
|
</Text>
|
|
</View>
|
|
<TouchableOpacity
|
|
onPress={() => setShowLandingPagePicker(true)}
|
|
style={{
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
backgroundColor: colors.backgroundSecondary,
|
|
paddingHorizontal: 20,
|
|
paddingVertical: 16,
|
|
}}
|
|
activeOpacity={0.7}
|
|
>
|
|
<View className="flex-row items-center">
|
|
<Ionicons name="home-outline" size={20} color={colors.text} />
|
|
<Text style={{ color: colors.text }} className="ml-3 text-base font-semibold">
|
|
First Page
|
|
</Text>
|
|
</View>
|
|
<View className="flex-row items-center">
|
|
<View
|
|
style={{
|
|
marginLeft: 12,
|
|
marginRight: 8,
|
|
borderRadius: 6,
|
|
backgroundColor: isDark ? "#3A3A3C" : "#F3F4F6",
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 4,
|
|
}}
|
|
>
|
|
<Text
|
|
style={{ color: isDark ? "#A3A3A3" : "#4B5563" }}
|
|
className="text-sm font-medium"
|
|
>
|
|
{landingPageLabel}
|
|
</Text>
|
|
</View>
|
|
<Ionicons name="chevron-forward" size={20} color={colors.textSecondary} />
|
|
</View>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Delete Account Link */}
|
|
<View
|
|
style={{
|
|
marginTop: 24,
|
|
borderRadius: 8,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
backgroundColor: colors.backgroundSecondary,
|
|
overflow: "hidden",
|
|
}}
|
|
>
|
|
<TouchableOpacity
|
|
onPress={() =>
|
|
openInAppBrowser("https://app.cal.com/settings/my-account/profile", "Delete Account")
|
|
}
|
|
style={{
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
backgroundColor: colors.backgroundSecondary,
|
|
paddingHorizontal: 20,
|
|
paddingVertical: 16,
|
|
}}
|
|
activeOpacity={0.7}
|
|
>
|
|
<View className="flex-1 flex-row items-center">
|
|
<Ionicons name="trash-outline" size={20} color={theme.destructive} />
|
|
<Text className="ml-3 text-base font-medium" style={{ color: theme.destructive }}>
|
|
Delete Account
|
|
</Text>
|
|
</View>
|
|
<Ionicons name="open-outline" size={20} color={colors.textSecondary} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Sign Out Button */}
|
|
<View
|
|
style={{
|
|
marginTop: 16,
|
|
borderRadius: 8,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
backgroundColor: colors.backgroundSecondary,
|
|
overflow: "hidden",
|
|
}}
|
|
>
|
|
<TouchableOpacity
|
|
onPress={handleSignOut}
|
|
style={{
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
backgroundColor: colors.backgroundSecondary,
|
|
paddingHorizontal: 20,
|
|
paddingVertical: 16,
|
|
}}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Ionicons name="log-out-outline" size={20} color={theme.destructive} />
|
|
<Text className="ml-2 text-base font-medium" style={{ color: theme.destructive }}>
|
|
Sign Out
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Footer Note */}
|
|
<Text
|
|
style={{ color: isDark ? "#6B7280" : "#9CA3AF" }}
|
|
className="mt-6 px-1 text-center text-xs"
|
|
>
|
|
The companion app is an extension of the web application.{"\n"}
|
|
For advanced features, visit{" "}
|
|
<Text style={{ color: isDark ? "#D1D5DB" : "#1F2937" }}>app.cal.com</Text>
|
|
</Text>
|
|
</ScrollView>
|
|
|
|
{/* Logout Confirmation Modal - kept for consistency */}
|
|
<LogoutConfirmModal
|
|
visible={showLogoutModal}
|
|
onConfirm={() => {
|
|
setShowLogoutModal(false);
|
|
performLogout();
|
|
}}
|
|
onCancel={() => setShowLogoutModal(false)}
|
|
/>
|
|
|
|
{/* Landing Page Picker */}
|
|
<LandingPagePicker
|
|
visible={showLandingPagePicker}
|
|
currentValue={preferences.landingPage}
|
|
onSelect={handleLandingPageSelect}
|
|
onClose={() => setShowLandingPagePicker(false)}
|
|
/>
|
|
</>
|
|
);
|
|
}
|