Files
Dhairyashil ShindeandGitHub fa53f54815 feat(companion): add configurable landing page (#27267)
* 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
2026-02-13 11:28:42 -03:00

199 lines
6.4 KiB
TypeScript

import { Ionicons } from "@expo/vector-icons";
import type { ColorValue, ImageSourcePropType } from "react-native";
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 = {
getImageSource: (name: string, size: number, color: ColorValue) => Promise<ImageSourcePropType>;
};
export default function TabLayout() {
const router = useRouter();
const { preferences, isLoading } = useUserPreferences();
const hasNavigated = useRef(false);
const colorScheme = useColorScheme();
const isDark = colorScheme === "dark";
const colors = {
selected: isDark ? "#FFFFFF" : "#000000",
inactive: "#A3A3A3",
background: isDark ? "#000000" : "#FFFFFF",
border: isDark ? "#4D4D4D" : "#C6C6C8",
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 <WebTabs colors={colors} />;
}
return (
<NativeTabs
tintColor={colors.selected}
iconColor={Platform.select({ android: colors.inactive, ios: undefined })}
indicatorColor={Platform.select({ android: colors.indicator, ios: undefined })}
backgroundColor={Platform.select({ android: colors.background, ios: undefined })}
labelStyle={{
default: { color: colors.inactive, fontSize: Platform.select({ android: 11, ios: 8.5 }) },
selected: { color: colors.selected, fontSize: Platform.select({ android: 12, ios: 10 }) },
}}
disableTransparentOnScrollEdge={true}
>
<NativeTabs.Trigger name="(bookings)">
{Platform.select({
ios: <NativeTabs.Trigger.Icon sf="calendar" />,
android: (
<NativeTabs.Trigger.Icon
src={<VectorIcon family={Ionicons as VectorIconFamily} name="calendar-outline" />}
selectedColor={colors.selected}
/>
),
})}
<NativeTabs.Trigger.Label>Bookings</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="(event-types)">
{Platform.select({
ios: <NativeTabs.Trigger.Icon sf="link" />,
android: (
<NativeTabs.Trigger.Icon
src={<VectorIcon family={Ionicons as VectorIconFamily} name="link-outline" />}
selectedColor={colors.selected}
/>
),
})}
<NativeTabs.Trigger.Label>Event Types</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="(availability)">
{Platform.select({
ios: <NativeTabs.Trigger.Icon sf={{ default: "clock", selected: "clock.fill" }} />,
android: (
<NativeTabs.Trigger.Icon
src={<VectorIcon family={Ionicons as VectorIconFamily} name="time-outline" />}
selectedColor={colors.selected}
/>
),
})}
<NativeTabs.Trigger.Label>Availability</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="(more)">
{Platform.select({
ios: <NativeTabs.Trigger.Icon sf={{ default: "ellipsis", selected: "ellipsis" }} />,
android: (
<NativeTabs.Trigger.Icon
src={<VectorIcon family={Ionicons as VectorIconFamily} name="ellipsis-horizontal" />}
selectedColor={colors.selected}
/>
),
})}
<NativeTabs.Trigger.Label>More</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
</NativeTabs>
);
}
type TabColors = {
selected: string;
inactive: string;
background: string;
border: string;
indicator: string;
};
// TODO: Remove this once native tabs are supported on web
function WebTabs({ colors }: { colors: TabColors }) {
return (
<Tabs
screenOptions={{
headerShown: false,
tabBarActiveTintColor: colors.selected,
tabBarInactiveTintColor: colors.inactive,
tabBarStyle: {
backgroundColor: colors.background,
borderTopWidth: 0.5,
borderTopColor: colors.border,
paddingBottom: 4,
paddingTop: 4,
height: Platform.OS === "android" ? 74 : 66,
},
}}
>
<Tabs.Screen
name="index"
options={{
href: null,
}}
/>
<Tabs.Screen
name="(event-types)"
options={{
title: "Event Types",
tabBarIcon: ({ color, focused }) => (
<Ionicons name={focused ? "link" : "link-outline"} size={24} color={color} />
),
}}
/>
<Tabs.Screen
name="(bookings)"
options={{
title: "Bookings",
tabBarIcon: ({ color, focused }) => (
<Ionicons name={focused ? "calendar" : "calendar-outline"} size={24} color={color} />
),
}}
/>
<Tabs.Screen
name="(availability)"
options={{
title: "Availability",
tabBarIcon: ({ color, focused }) => (
<Ionicons name={focused ? "time" : "time-outline"} size={24} color={color} />
),
}}
/>
<Tabs.Screen
name="(more)"
options={{
title: "More",
tabBarIcon: ({ color, focused }) => (
<Ionicons
name={focused ? "ellipsis-horizontal" : "ellipsis-horizontal-outline"}
size={24}
color={color}
/>
),
}}
/>
</Tabs>
);
}