239 lines
7.0 KiB
TypeScript
239 lines
7.0 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
|
import {
|
|
createMaterialTopTabNavigator,
|
|
MaterialTopTabNavigationOptions,
|
|
MaterialTopTabNavigationEventMap,
|
|
MaterialTopTabBarProps,
|
|
} from "@react-navigation/material-top-tabs";
|
|
import { withLayoutContext } from "expo-router";
|
|
import { ParamListBase, TabNavigationState } from "@react-navigation/native";
|
|
import { useAuthStore } from "~/lib/stores/authStore";
|
|
import { useUserProfileStore } from "~/lib/stores/userProfileStore";
|
|
import { router } from "expo-router";
|
|
import { View, Pressable, StyleSheet, Image, Platform } from "react-native";
|
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
import { useTabStore } from "~/lib/stores";
|
|
import { Icons } from "~/assets/icons";
|
|
import {
|
|
History,
|
|
CalendarClock,
|
|
ListChecks,
|
|
CalendarRange,
|
|
ListChecksIcon,
|
|
} from "lucide-react-native";
|
|
|
|
const { Navigator } = createMaterialTopTabNavigator();
|
|
|
|
// Wrap the navigator with expo-router's layout context
|
|
export const MaterialTopTabs = withLayoutContext<
|
|
MaterialTopTabNavigationOptions,
|
|
typeof Navigator,
|
|
TabNavigationState<ParamListBase>,
|
|
MaterialTopTabNavigationEventMap
|
|
>(Navigator);
|
|
|
|
// Screen title mapping - automatically includes all screens in (tabs) folder
|
|
// Add new screens here as you create them in the (tabs) folder
|
|
const screenTitles: Record<string, string> = {
|
|
index: "Home",
|
|
schedules: "Schedules",
|
|
requests: "Requests",
|
|
listrecipient: "Recipients",
|
|
history: "Transactions",
|
|
};
|
|
|
|
const TAB_BAR_HEIGHT = 72;
|
|
|
|
const tabIcons: Record<string, (color: string) => React.ReactNode> = {
|
|
index: (color: string) => (
|
|
<Image
|
|
source={Icons.homeIcon}
|
|
style={{ width: 32, height: 32, tintColor: color }}
|
|
resizeMode="contain"
|
|
/>
|
|
),
|
|
schedules: (color: string) => <CalendarRange color={color} size={26} />,
|
|
requests: (color: string) => <ListChecksIcon color={color} size={26} />,
|
|
listrecipient: (color: string) => (
|
|
<Image
|
|
source={Icons.searchIcon}
|
|
style={{ width: 32, height: 32, tintColor: color }}
|
|
resizeMode="contain"
|
|
/>
|
|
),
|
|
history: (color: string) => <History color={color} size={26} />,
|
|
};
|
|
|
|
const getHrefForRoute = (routeName: string) =>
|
|
routeName === "index" ? "/" : `/(tabs)/${routeName}`;
|
|
|
|
// Primary green color used across the app (matches app's light theme)
|
|
const PRIMARY_COLOR = "hsl(147,55%,28%)";
|
|
const INACTIVE_COLOR = "#d1d5db";
|
|
|
|
function CustomTabBar({ state, navigation }: MaterialTopTabBarProps) {
|
|
const { setLastVisitedTab } = useTabStore();
|
|
const [selectedIndex, setSelectedIndex] = React.useState(state.index);
|
|
const insets = useSafeAreaInsets();
|
|
|
|
React.useEffect(() => {
|
|
setSelectedIndex(state.index);
|
|
}, [state.index]);
|
|
|
|
// App uses light theme - use primary green color
|
|
const activeColor = PRIMARY_COLOR;
|
|
const inactiveColor = INACTIVE_COLOR;
|
|
|
|
// Calculate bottom padding: use reduced safe area inset on iOS, or 16px on Android
|
|
const bottomPadding =
|
|
Platform.OS === "ios" ? Math.max(insets.bottom * 0.6, 8) : 16;
|
|
|
|
return (
|
|
<View style={[styles.tabBarContainer, { paddingBottom: bottomPadding }]}>
|
|
{state.routes.map((route, index) => {
|
|
const focused = selectedIndex === index;
|
|
const href = getHrefForRoute(route.name);
|
|
const Icon = tabIcons[route.name];
|
|
|
|
const onPress = () => {
|
|
setSelectedIndex(index);
|
|
const event = navigation.emit({
|
|
type: "tabPress",
|
|
target: route.key,
|
|
canPreventDefault: true,
|
|
});
|
|
|
|
if (!focused && !event.defaultPrevented) {
|
|
setLastVisitedTab(href);
|
|
navigation.navigate(route.name);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Pressable
|
|
key={route.key}
|
|
onPress={onPress}
|
|
style={styles.tabButton}
|
|
hitSlop={12}
|
|
>
|
|
{Icon ? Icon(focused ? activeColor : inactiveColor) : null}
|
|
</Pressable>
|
|
);
|
|
})}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
export default function TabLayout() {
|
|
const { user, loading } = useAuthStore();
|
|
const { profiles } = useUserProfileStore();
|
|
const [isAgent, setIsAgent] = React.useState<boolean | null>(null);
|
|
const [checkingAgent, setCheckingAgent] = React.useState(false);
|
|
|
|
// Get the current user's profile entry
|
|
const profileEntry = user?.uid ? profiles[user.uid] : null;
|
|
const profile = profileEntry?.profile;
|
|
const profileLoading = profileEntry?.loading ?? false;
|
|
|
|
const isDevEmulatorUser = __DEV__ && user?.uid === "dev-emulator-user";
|
|
|
|
// Check if user is an agent when profile is not available
|
|
useEffect(() => {
|
|
if (!user || profile || checkingAgent || isAgent !== null) {
|
|
return;
|
|
}
|
|
|
|
const checkAgent = async () => {
|
|
setCheckingAgent(true);
|
|
try {
|
|
const { AuthService } = await import("~/lib/services/authServices");
|
|
const agentExists = await AuthService.checkAgentExists(user.uid);
|
|
setIsAgent(agentExists);
|
|
} catch (error) {
|
|
console.error('TabLayout - error checking agent:', error);
|
|
setIsAgent(false);
|
|
} finally {
|
|
setCheckingAgent(false);
|
|
}
|
|
};
|
|
|
|
checkAgent();
|
|
}, [user, profile, checkingAgent, isAgent]);
|
|
|
|
// Redirect to agent signup if not authenticated or no profile/agent
|
|
useEffect(() => {
|
|
if (!loading && !profileLoading && !checkingAgent) {
|
|
// In dev, allow the fake emulator user even without a profile
|
|
if (isDevEmulatorUser) {
|
|
return;
|
|
}
|
|
|
|
if (!user || (!profile && isAgent === false)) {
|
|
console.log(
|
|
"TabLayout - redirecting to agent signin, user:",
|
|
!!user,
|
|
"profile:",
|
|
!!profile,
|
|
"isAgent:",
|
|
isAgent
|
|
);
|
|
router.replace("/auth/agent-signin");
|
|
}
|
|
}
|
|
}, [user, loading, profile, profileLoading, isDevEmulatorUser, isAgent, checkingAgent]);
|
|
|
|
// Don't render tabs if not authenticated or no profile/agent
|
|
// In dev, still render for the emulator user even without profile
|
|
if (!isDevEmulatorUser && (loading || profileLoading || checkingAgent || !user || (!profile && isAgent !== true))) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<MaterialTopTabs
|
|
screenOptions={{
|
|
swipeEnabled: true,
|
|
lazy: true,
|
|
tabBarIndicatorStyle: {
|
|
backgroundColor: "transparent",
|
|
},
|
|
tabBarItemStyle: {
|
|
flex: 1,
|
|
},
|
|
}}
|
|
tabBar={(props) => <CustomTabBar {...props} />}
|
|
>
|
|
{Object.entries(screenTitles).map(([name, title]) => (
|
|
<MaterialTopTabs.Screen
|
|
key={name}
|
|
name={name}
|
|
options={{
|
|
title,
|
|
}}
|
|
/>
|
|
))}
|
|
</MaterialTopTabs>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
tabBarContainer: {
|
|
position: "absolute",
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-around",
|
|
paddingTop: 16,
|
|
paddingHorizontal: 4,
|
|
backgroundColor: "#ffffff", // tailwind gray-50
|
|
borderTopWidth: StyleSheet.hairlineWidth,
|
|
borderTopColor: "#e5e7eb", // tailwind gray-200
|
|
zIndex: 10,
|
|
},
|
|
tabButton: {
|
|
flex: 1,
|
|
alignItems: "center",
|
|
},
|
|
});
|