diff --git a/companion/app.json b/companion/app.json
index 9cb1e75570..6238a8ef1b 100644
--- a/companion/app.json
+++ b/companion/app.json
@@ -3,7 +3,7 @@
"name": "Calcom",
"slug": "calcom-companion",
"scheme": "calcom",
- "version": "1.0.3",
+ "version": "1.0.4",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
@@ -102,7 +102,7 @@
"minWidth": "180dp",
"minHeight": "110dp",
"description": "View your upcoming Cal.com bookings",
- "previewImage": "./assets/icon.png",
+ "previewImage": "./assets/widget-preview.png",
"resizeMode": "horizontal|vertical",
"targetCellWidth": 3,
"targetCellHeight": 2
diff --git a/companion/app/(tabs)/(availability)/index.tsx b/companion/app/(tabs)/(availability)/index.tsx
index 09ebc96bdb..5d064aabbf 100644
--- a/companion/app/(tabs)/(availability)/index.tsx
+++ b/companion/app/(tabs)/(availability)/index.tsx
@@ -100,14 +100,10 @@ export default function Availability() {
>
Availability
- {/* New Menu */}
-
+ {/* New Button - directly opens create dialog */}
+
-
-
- New Availability
-
-
+
{/* Profile Button */}
{userProfile?.avatarUrl ? (
diff --git a/companion/app/(tabs)/(bookings)/index.ios.tsx b/companion/app/(tabs)/(bookings)/index.ios.tsx
index 2953f5dabf..f9bcb4cb4a 100644
--- a/companion/app/(tabs)/(bookings)/index.ios.tsx
+++ b/companion/app/(tabs)/(bookings)/index.ios.tsx
@@ -1,21 +1,36 @@
import type { NativeStackHeaderItemMenuAction } from "@react-navigation/native-stack";
import { isLiquidGlassAvailable } from "expo-glass-effect";
-import { Stack } from "expo-router";
-import { useState } from "react";
+import { Stack, useLocalSearchParams } from "expo-router";
+import { useState, useEffect, useRef } from "react";
import { useColorScheme } from "react-native";
import { BookingListScreen } from "@/components/booking-list-screen/BookingListScreen";
import { useEventTypes } from "@/hooks";
-import { useActiveBookingFilter } from "@/hooks/useActiveBookingFilter";
+import { type BookingFilter, useActiveBookingFilter } from "@/hooks/useActiveBookingFilter";
+
+const VALID_FILTERS: BookingFilter[] = [
+ "upcoming",
+ "unconfirmed",
+ "recurring",
+ "past",
+ "cancelled",
+];
+
+function isValidBookingFilter(value: string | undefined): value is BookingFilter {
+ return value !== undefined && VALID_FILTERS.includes(value as BookingFilter);
+}
export default function Bookings() {
+ const { filter } = useLocalSearchParams<{ filter?: string }>();
+ const initialFilter = isValidBookingFilter(filter) ? filter : "upcoming";
+
const [searchQuery, setSearchQuery] = useState("");
const [selectedEventTypeId, setSelectedEventTypeId] = useState(null);
const { data: eventTypes } = useEventTypes();
// Use the active booking filter hook
const { activeFilter, filterOptions, filterParams, handleFilterChange } = useActiveBookingFilter(
- "upcoming",
+ initialFilter,
() => {
// Clear dependent filters when status filter changes
setSearchQuery("");
@@ -23,6 +38,21 @@ export default function Bookings() {
}
);
+ // Track if we want to ignore the next activeFilter change (because it came from URL sync)
+ const lastUrlFilter = useRef(null);
+
+ // Update filter when URL params change (for when component is already mounted)
+ useEffect(() => {
+ if (
+ isValidBookingFilter(filter) &&
+ filter !== activeFilter &&
+ filter !== lastUrlFilter.current
+ ) {
+ lastUrlFilter.current = filter;
+ handleFilterChange(filter);
+ }
+ }, [filter, activeFilter, handleFilterChange]);
+
const colorScheme = useColorScheme();
const isDark = colorScheme === "dark";
const textColor = isDark ? "#FFFFFF" : "#000000";
diff --git a/companion/app/(tabs)/(bookings)/index.tsx b/companion/app/(tabs)/(bookings)/index.tsx
index 51e20e4d62..27b11ab34e 100644
--- a/companion/app/(tabs)/(bookings)/index.tsx
+++ b/companion/app/(tabs)/(bookings)/index.tsx
@@ -1,5 +1,6 @@
import { Ionicons } from "@expo/vector-icons";
-import { useState } from "react";
+import { useLocalSearchParams } from "expo-router";
+import { useState, useEffect, useRef } from "react";
import { Text, TextInput, useColorScheme, View } from "react-native";
import { BookingListScreen } from "@/components/booking-list-screen/BookingListScreen";
import { Header } from "@/components/Header";
@@ -10,11 +11,26 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { AppPressable } from "@/components/AppPressable";
-import { useActiveBookingFilter } from "@/hooks/useActiveBookingFilter";
+import { type BookingFilter, useActiveBookingFilter } from "@/hooks/useActiveBookingFilter";
import { useEventTypes } from "@/hooks";
import { getColors } from "@/constants/colors";
+const VALID_FILTERS: BookingFilter[] = [
+ "upcoming",
+ "unconfirmed",
+ "recurring",
+ "past",
+ "cancelled",
+];
+
+function isValidBookingFilter(value: string | undefined): value is BookingFilter {
+ return value !== undefined && VALID_FILTERS.includes(value as BookingFilter);
+}
+
export default function Bookings() {
+ const { filter } = useLocalSearchParams<{ filter?: string }>();
+ const initialFilter = isValidBookingFilter(filter) ? filter : "upcoming";
+
const [searchQuery, setSearchQuery] = useState("");
const [selectedEventTypeId, setSelectedEventTypeId] = useState(null);
const [selectedEventTypeLabel, setSelectedEventTypeLabel] = useState(null);
@@ -26,15 +42,37 @@ export default function Bookings() {
const { data: eventTypes = [], isLoading: eventTypesLoading } = useEventTypes();
// Use the active booking filter hook
- const { activeFilter, filterOptions, filterParams, handleFilterChange } = useActiveBookingFilter(
- "upcoming",
- () => {
- // Clear dependent filters when status filter changes
- setSearchQuery("");
- setSelectedEventTypeId(null);
- setSelectedEventTypeLabel(null);
+ const {
+ activeFilter,
+ filterOptions,
+ filterParams,
+ handleFilterChange: originalHandleFilterChange,
+ } = useActiveBookingFilter(initialFilter, () => {
+ // Clear dependent filters when status filter changes
+ setSearchQuery("");
+ setSelectedEventTypeId(null);
+ setSelectedEventTypeLabel(null);
+ });
+
+ // Wrap handleFilterChange with debug logging
+ const handleFilterChange = (filter: string) => {
+ originalHandleFilterChange(filter as BookingFilter);
+ };
+
+ // Track if we want to ignore the next activeFilter change (because it came from URL sync)
+ const lastUrlFilter = useRef(null);
+
+ // Reactively update filter when URL params change (e.g., from deep link)
+ useEffect(() => {
+ if (
+ isValidBookingFilter(filter) &&
+ filter !== activeFilter &&
+ filter !== lastUrlFilter.current
+ ) {
+ lastUrlFilter.current = filter;
+ originalHandleFilterChange(filter);
}
- );
+ }, [filter, activeFilter, originalHandleFilterChange]);
const handleSearch = (query: string) => {
setSearchQuery(query);
diff --git a/companion/app/(tabs)/(more)/index.ios.tsx b/companion/app/(tabs)/(more)/index.ios.tsx
index 8a64080af8..205ae8d389 100644
--- a/companion/app/(tabs)/(more)/index.ios.tsx
+++ b/companion/app/(tabs)/(more)/index.ios.tsx
@@ -12,11 +12,13 @@ import {
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 { showErrorAlert, showNotAvailableAlert } from "@/utils/alerts";
+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";
@@ -33,7 +35,9 @@ 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";
@@ -48,6 +52,15 @@ export default function More() {
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
@@ -183,6 +196,74 @@ export default function More() {
))}
+ {/* App Settings */}
+
+
+
+ App Settings
+
+
+ setShowLandingPagePicker(true)}
+ style={{
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ backgroundColor: colors.backgroundSecondary,
+ paddingHorizontal: 20,
+ paddingVertical: 16,
+ }}
+ activeOpacity={0.7}
+ >
+
+
+
+ First Page
+
+
+
+
+
+ {landingPageLabel}
+
+
+
+
+
+
+
{/* Delete Account Link */}
setShowLogoutModal(false)}
/>
+
+ {/* Landing Page Picker */}
+ setShowLandingPagePicker(false)}
+ />
>
);
}
diff --git a/companion/app/(tabs)/(more)/index.tsx b/companion/app/(tabs)/(more)/index.tsx
index 8166e1961e..0479ce894e 100644
--- a/companion/app/(tabs)/(more)/index.tsx
+++ b/companion/app/(tabs)/(more)/index.tsx
@@ -10,10 +10,12 @@ import {
View,
} from "react-native";
import { Header } from "@/components/Header";
+import { LandingPagePicker } from "@/components/LandingPagePicker";
import { LogoutConfirmModal } from "@/components/LogoutConfirmModal";
import { useAuth } from "@/contexts/AuthContext";
import { useQueryContext } from "@/contexts/QueryContext";
-import { showErrorAlert } from "@/utils/alerts";
+import { type LandingPage, useUserPreferences } from "@/hooks/useUserPreferences";
+import { showErrorAlert, showSuccessAlert } from "@/utils/alerts";
import { openInAppBrowser } from "@/utils/browser";
import { getColors } from "@/constants/colors";
@@ -28,11 +30,22 @@ interface MoreMenuItem {
export default function More() {
const { logout } = useAuth();
const { clearCache } = useQueryContext();
+ const { preferences, setLandingPage, landingPageLabel } = useUserPreferences();
const [showLogoutModal, setShowLogoutModal] = useState(false);
+ const [showLandingPagePicker, setShowLandingPagePicker] = useState(false);
const colorScheme = useColorScheme();
const isDark = colorScheme === "dark";
const theme = getColors(isDark);
+ 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
@@ -136,6 +149,49 @@ export default function More() {
))}
+ {/* App Settings */}
+
+
+
+ App Settings
+
+
+ setShowLandingPagePicker(true)}
+ className="flex-row items-center justify-between bg-white px-5 py-4 active:bg-[#F8F9FA]"
+ style={{ backgroundColor: theme.backgroundSecondary }}
+ >
+
+
+
+ First Page
+
+
+
+
+
+ {landingPageLabel}
+
+
+
+
+
+
{/* Delete Account Link */}
setShowLogoutModal(false)}
/>
+
+ {/* Landing Page Picker Modal */}
+ setShowLandingPagePicker(false)}
+ />
);
}
diff --git a/companion/app/(tabs)/_layout.tsx b/companion/app/(tabs)/_layout.tsx
index d94c024308..dd72a6c896 100644
--- a/companion/app/(tabs)/_layout.tsx
+++ b/companion/app/(tabs)/_layout.tsx
@@ -1,9 +1,11 @@
import { Ionicons } from "@expo/vector-icons";
import type { ColorValue, ImageSourcePropType } from "react-native";
-import { Tabs, VectorIcon } from "expo-router";
+import { Tabs, VectorIcon, useRouter } from "expo-router";
import { NativeTabs } from "expo-router/unstable-native-tabs";
import { useColorScheme } from "react-native";
import { Platform } from "react-native";
+import { useEffect, useRef } from "react";
+import { getRouteFromPreference, useUserPreferences } from "@/hooks/useUserPreferences";
// Type for vector icon families that support getImageSource
type VectorIconFamily = {
@@ -11,6 +13,9 @@ type VectorIconFamily = {
};
export default function TabLayout() {
+ const router = useRouter();
+ const { preferences, isLoading } = useUserPreferences();
+ const hasNavigated = useRef(false);
const colorScheme = useColorScheme();
const isDark = colorScheme === "dark";
@@ -22,6 +27,26 @@ export default function TabLayout() {
indicator: isDark ? "#FFFFFF15" : "#00000015",
};
+ // Navigate to preferred landing page on first load
+ // Since Bookings is the default first tab, we only navigate if preference is different
+ useEffect(() => {
+ if (!isLoading && !hasNavigated.current) {
+ hasNavigated.current = true;
+
+ // Only navigate if preference is not the default (bookings)
+ if (preferences.landingPage !== "bookings") {
+ const route = getRouteFromPreference(preferences.landingPage);
+ console.log("[TabLayout] Navigating to preferred landing page:", route);
+
+ // Use a minimal delay to ensure NativeTabs is initialized
+ setTimeout(() => {
+ // biome-ignore lint/suspicious/noExplicitAny: expo-router types don't support dynamic route strings from preferences
+ router.navigate(route as any);
+ }, 50);
+ }
+ }
+ }, [isLoading, preferences.landingPage, router]);
+
if (Platform.OS === "web") {
return ;
}
@@ -38,19 +63,6 @@ export default function TabLayout() {
}}
disableTransparentOnScrollEdge={true}
>
-
- {Platform.select({
- ios: ,
- android: (
- }
- selectedColor={colors.selected}
- />
- ),
- })}
- Event Types
-
-
{Platform.select({
ios: ,
@@ -64,6 +76,19 @@ export default function TabLayout() {
Bookings
+
+ {Platform.select({
+ ios: ,
+ android: (
+ }
+ selectedColor={colors.selected}
+ />
+ ),
+ })}
+ Event Types
+
+
{Platform.select({
ios: ,
diff --git a/companion/app/(tabs)/index.tsx b/companion/app/(tabs)/index.tsx
index d986559187..a1ac454952 100644
--- a/companion/app/(tabs)/index.tsx
+++ b/companion/app/(tabs)/index.tsx
@@ -1,5 +1,21 @@
import { Redirect } from "expo-router";
+import { ActivityIndicator, View } from "react-native";
+import { getRouteFromPreference, useUserPreferences } from "@/hooks/useUserPreferences";
export default function TabsIndex() {
- return ;
+ const { preferences, isLoading } = useUserPreferences();
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ const route = getRouteFromPreference(preferences.landingPage);
+ console.log("[TabsIndex] Redirecting to:", route, "from preference:", preferences.landingPage);
+
+ // biome-ignore lint/suspicious/noExplicitAny: expo-router types don't support dynamic route strings from preferences
+ return ;
}
diff --git a/companion/assets/widget-preview.png b/companion/assets/widget-preview.png
new file mode 100644
index 0000000000..c5db4c4d9e
Binary files /dev/null and b/companion/assets/widget-preview.png differ
diff --git a/companion/components/EmptyScreen.tsx b/companion/components/EmptyScreen.tsx
index 8610e78315..a218477895 100644
--- a/companion/components/EmptyScreen.tsx
+++ b/companion/components/EmptyScreen.tsx
@@ -26,9 +26,7 @@ export function EmptyScreen({
const colors = {
text: isDark ? "#FFFFFF" : "#111827",
textSecondary: isDark ? "#A3A3A3" : "#6B7280",
- background: isDark ? "#171717" : "#FFFFFF",
- backgroundSecondary: isDark ? "#2C2C2E" : "#F3F4F6",
- border: isDark ? "#4D4D4D" : "#D1D5DB",
+ iconBackground: isDark ? "#2C2C2E" : "#F3F4F6",
icon: isDark ? "#E5E5EA" : "#374151",
buttonBg: isDark ? "#FFFFFF" : "#111827",
buttonText: isDark ? "#000000" : "#FFFFFF",
@@ -37,9 +35,6 @@ export function EmptyScreen({
return (
void;
+ onClose: () => void;
+}
+
+export function LandingPagePicker({
+ visible,
+ currentValue: _currentValue,
+ onSelect,
+ onClose,
+}: LandingPagePickerProps) {
+ const sheetShownRef = useRef(false);
+
+ useEffect(() => {
+ if (visible && !sheetShownRef.current) {
+ sheetShownRef.current = true;
+
+ const options = [...LANDING_PAGE_OPTIONS.map((opt) => opt.label), "Cancel"];
+ const cancelButtonIndex = options.length - 1;
+
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ title: "First Page",
+ message: "Choose which page opens when you launch the app",
+ options,
+ cancelButtonIndex,
+ destructiveButtonIndex: undefined,
+ },
+ (buttonIndex) => {
+ if (buttonIndex !== cancelButtonIndex && buttonIndex < LANDING_PAGE_OPTIONS.length) {
+ const selectedOption = LANDING_PAGE_OPTIONS[buttonIndex];
+ if (selectedOption) {
+ onSelect(selectedOption.value);
+ }
+ }
+ onClose();
+ }
+ );
+ }
+
+ if (!visible) {
+ sheetShownRef.current = false;
+ }
+ }, [visible, onSelect, onClose]);
+
+ return null;
+}
diff --git a/companion/components/LandingPagePicker.tsx b/companion/components/LandingPagePicker.tsx
new file mode 100644
index 0000000000..32b2f84878
--- /dev/null
+++ b/companion/components/LandingPagePicker.tsx
@@ -0,0 +1,75 @@
+import { Ionicons } from "@expo/vector-icons";
+import { Platform, Text, TouchableOpacity, View } from "react-native";
+import { FullScreenModal } from "./FullScreenModal";
+import {
+ LANDING_PAGE_OPTIONS,
+ type LandingPage,
+ type LandingPageOption,
+} from "@/hooks/useUserPreferences";
+
+interface LandingPagePickerProps {
+ visible: boolean;
+ currentValue: LandingPage;
+ onSelect: (value: LandingPage) => void;
+ onClose: () => void;
+}
+
+export function LandingPagePicker({
+ visible,
+ currentValue,
+ onSelect,
+ onClose,
+}: LandingPagePickerProps) {
+ const handleSelect = (option: LandingPageOption) => {
+ onSelect(option.value);
+ onClose();
+ };
+
+ return (
+
+
+
+
+ First Page
+
+ Choose which page opens when you launch the app
+
+
+
+
+ {LANDING_PAGE_OPTIONS.map((option) => {
+ const isSelected = currentValue === option.value;
+ return (
+ handleSelect(option)}
+ className="flex-row items-center justify-between px-4 py-3 active:bg-gray-100"
+ style={Platform.OS === "web" ? { cursor: "pointer" } : undefined}
+ activeOpacity={0.7}
+ >
+
+ {option.label}
+
+ {isSelected && }
+
+ );
+ })}
+
+
+
+
+ Cancel
+
+
+
+
+
+ );
+}
diff --git a/companion/components/booking-list-screen/BookingListScreen.tsx b/companion/components/booking-list-screen/BookingListScreen.tsx
index 8a6e690c8e..a026433d41 100644
--- a/companion/components/booking-list-screen/BookingListScreen.tsx
+++ b/companion/components/booking-list-screen/BookingListScreen.tsx
@@ -1,6 +1,6 @@
import { Ionicons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
-import React, { Activity, useMemo, useState } from "react";
+import React, { useMemo, useState } from "react";
import {
Alert,
FlatList,
@@ -677,7 +677,7 @@ export const BookingListScreen: React.FC = ({
{renderFilterControls?.()}
{/* Empty state - no bookings */}
-
+ {showEmptyState && (
= ({
/>
-
+ )}
{/* Search empty state */}
-
+ {showSearchEmptyState && (
= ({
/>
-
+ )}
{/* Bookings list */}
-
- {isManualRefreshing ? (
+ {showList &&
+ (isManualRefreshing ? (
+ ) : iosStyle ? (
+ item.key}
+ renderItem={renderListItem}
+ contentContainerStyle={{ paddingBottom: 90 }}
+ refreshControl={}
+ showsVerticalScrollIndicator={false}
+ contentInsetAdjustmentBehavior="automatic"
+ style={{ backgroundColor: isDark ? "#000000" : "white" }}
+ />
) : (
- <>
-
+
+
item.key}
@@ -734,28 +745,10 @@ export const BookingListScreen: React.FC = ({
contentContainerStyle={{ paddingBottom: 90 }}
refreshControl={}
showsVerticalScrollIndicator={false}
- contentInsetAdjustmentBehavior="automatic"
- style={{ backgroundColor: isDark ? "#000000" : "white" }}
/>
-
-
-
-
-
- item.key}
- renderItem={renderListItem}
- contentContainerStyle={{ paddingBottom: 90 }}
- refreshControl={}
- showsVerticalScrollIndicator={false}
- />
-
-
-
- >
- )}
-
+
+
+ ))}
{/* Modals */}
{
try {
await clearAuth();
+ // Clear user preferences to ensure fresh state for next user
+ try {
+ await generalStorage.removeItem(USER_PREFERENCES_KEY);
+ } catch (prefsError) {
+ console.warn("Failed to clear user preferences during logout:", prefsError);
+ }
// Clear all cached queries to ensure fresh data on re-login
try {
await clearQueryCache();
diff --git a/companion/hooks/index.ts b/companion/hooks/index.ts
index f40491509b..98d341fef9 100644
--- a/companion/hooks/index.ts
+++ b/companion/hooks/index.ts
@@ -65,6 +65,15 @@ export {
} from "./useSchedules";
// Toast hook
export { type ToastState, type ToastType, useToast } from "./useToast";
+// User Preferences hooks
+export {
+ getRouteFromPreference,
+ LANDING_PAGE_OPTIONS,
+ type LandingPage,
+ type LandingPageOption,
+ type UserPreferences,
+ useUserPreferences,
+} from "./useUserPreferences";
// User Profile hooks
export {
type UpdateUserProfileInput,
diff --git a/companion/hooks/useUserPreferences.ts b/companion/hooks/useUserPreferences.ts
new file mode 100644
index 0000000000..9b9ba89ee0
--- /dev/null
+++ b/companion/hooks/useUserPreferences.ts
@@ -0,0 +1,92 @@
+import { useCallback, useEffect, useState } from "react";
+import { generalStorage } from "@/utils/storage";
+
+export type LandingPage = "event-types" | "bookings" | "bookings:unconfirmed";
+
+export interface UserPreferences {
+ landingPage: LandingPage;
+}
+
+export const USER_PREFERENCES_KEY = "cal_user_preferences";
+
+const DEFAULT_PREFERENCES: UserPreferences = {
+ landingPage: "event-types",
+};
+
+export interface LandingPageOption {
+ value: LandingPage;
+ label: string;
+}
+
+export const LANDING_PAGE_OPTIONS: LandingPageOption[] = [
+ { value: "event-types", label: "Event Types" },
+ { value: "bookings", label: "Bookings" },
+ { value: "bookings:unconfirmed", label: "Bookings (Unconfirmed)" },
+];
+
+export function getLandingPageLabel(landingPage: LandingPage): string {
+ const option = LANDING_PAGE_OPTIONS.find((opt) => opt.value === landingPage);
+ return option?.label ?? "Event Types";
+}
+
+export function getRouteFromPreference(landingPage: LandingPage): string {
+ switch (landingPage) {
+ case "event-types":
+ return "/(tabs)/(event-types)";
+ case "bookings":
+ return "/(tabs)/(bookings)";
+ case "bookings:unconfirmed":
+ return "/(tabs)/(bookings)?filter=unconfirmed";
+ default:
+ return "/(tabs)/(event-types)";
+ }
+}
+
+export function useUserPreferences() {
+ const [preferences, setPreferences] = useState(DEFAULT_PREFERENCES);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const loadPreferences = async () => {
+ try {
+ const stored = await generalStorage.getItem(USER_PREFERENCES_KEY);
+ if (stored) {
+ const parsed = JSON.parse(stored) as UserPreferences;
+ setPreferences(parsed);
+ }
+ setIsLoading(false);
+ } catch {
+ console.warn("Failed to load user preferences");
+ setIsLoading(false);
+ }
+ };
+
+ loadPreferences();
+ }, []);
+
+ const setLandingPage = useCallback(
+ async (landingPage: LandingPage): Promise => {
+ const newPreferences: UserPreferences = {
+ ...preferences,
+ landingPage,
+ };
+
+ setPreferences(newPreferences);
+
+ try {
+ await generalStorage.setItem(USER_PREFERENCES_KEY, JSON.stringify(newPreferences));
+ } catch (error) {
+ console.error("Failed to save user preferences:", error);
+ throw error;
+ }
+ },
+ [preferences]
+ );
+
+ return {
+ preferences,
+ isLoading,
+ setLandingPage,
+ landingPageLabel: getLandingPageLabel(preferences.landingPage),
+ };
+}