fix(companion): Android bottom navbar styling and iOS event type spacing issues (#26981)

* refactor: android bottom navbar color

* fix bottom navbar color for android and new account can log in now

* fix ios event type item spacing issue
This commit is contained in:
Dhairyashil Shinde
2026-01-19 13:59:11 +01:00
committed by GitHub
parent 950f9555ee
commit 28bf058b16
6 changed files with 110 additions and 27 deletions
+5 -1
View File
@@ -6,6 +6,7 @@ import { useState } from "react";
import { Alert, Pressable, ScrollView, Text, TouchableOpacity, View } from "react-native";
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 { openInAppBrowser } from "@/utils/browser";
@@ -22,11 +23,14 @@ interface MoreMenuItem {
export default function More() {
const router = useRouter();
const { logout } = useAuth();
const { clearCache } = useQueryContext();
const [showLogoutModal, setShowLogoutModal] = useState(false);
const { data: userProfile } = useUserProfile();
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);
@@ -111,7 +115,7 @@ export default function More() {
{/* Content */}
<ScrollView
style={{ backgroundColor: "#f8f9fa" }}
style={{ backgroundColor: "white" }}
contentContainerStyle={{ padding: 16, paddingBottom: 120 }}
showsVerticalScrollIndicator={false}
contentInsetAdjustmentBehavior="automatic"
+5 -1
View File
@@ -4,6 +4,7 @@ import { Alert, Platform, ScrollView, Text, TouchableOpacity, View } from "react
import { Header } from "@/components/Header";
import { LogoutConfirmModal } from "@/components/LogoutConfirmModal";
import { useAuth } from "@/contexts/AuthContext";
import { useQueryContext } from "@/contexts/QueryContext";
import { showErrorAlert } from "@/utils/alerts";
import { openInAppBrowser } from "@/utils/browser";
@@ -17,10 +18,13 @@ interface MoreMenuItem {
export default function More() {
const { logout } = useAuth();
const { clearCache } = useQueryContext();
const [showLogoutModal, setShowLogoutModal] = useState(false);
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);
@@ -84,7 +88,7 @@ export default function More() {
];
return (
<View className="flex-1 bg-[#f8f9fa]">
<View className="flex-1 bg-white">
<Header />
<ScrollView
contentContainerStyle={{ padding: 16, paddingBottom: 90 }}
+51 -20
View File
@@ -1,8 +1,16 @@
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { Ionicons } from "@expo/vector-icons";
import type { ColorValue, ImageSourcePropType } from "react-native";
import { Tabs, VectorIcon } from "expo-router";
import { NativeTabs } from "expo-router/unstable-native-tabs";
import { Platform } from "react-native";
// Type for vector icon families that support getImageSource
type VectorIconFamily = {
getImageSource: (name: string, size: number, color: ColorValue) => Promise<ImageSourcePropType>;
};
const SELECTED_COLOR = "#000000";
export default function TabLayout() {
if (Platform.OS === "web") {
return <WebTabs />;
@@ -10,42 +18,65 @@ export default function TabLayout() {
return (
<NativeTabs
tintColor="#000000" // Base tint color (black for selected)
tintColor={SELECTED_COLOR}
iconColor={Platform.select({ android: "#8E8E93", ios: undefined })}
indicatorColor={Platform.select({ android: "#00000015", ios: undefined })}
backgroundColor={Platform.select({ android: "#FFFFFFFF", ios: undefined })}
labelStyle={{
default: { color: "#8E8E93", fontSize: 8.5 }, // Gray text when unselected
selected: { color: "#000000", fontSize: 10 }, // Black text when selected
default: { color: "#8E8E93", fontSize: Platform.select({ android: 11, ios: 8.5 }) },
selected: { color: SELECTED_COLOR, fontSize: Platform.select({ android: 12, ios: 10 }) },
}}
disableTransparentOnScrollEdge={true}
>
<NativeTabs.Trigger name="(event-types)">
<NativeTabs.Trigger.Icon
sf="link"
src={<VectorIcon family={MaterialCommunityIcons} name="link" />}
/>
{Platform.select({
ios: <NativeTabs.Trigger.Icon sf="link" />,
android: (
<NativeTabs.Trigger.Icon
src={<VectorIcon family={Ionicons as VectorIconFamily} name="link-outline" />}
selectedColor={SELECTED_COLOR}
/>
),
})}
<NativeTabs.Trigger.Label>Event Types</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="(bookings)">
<NativeTabs.Trigger.Icon
sf="calendar"
src={<VectorIcon family={MaterialCommunityIcons} name="calendar" />}
/>
{Platform.select({
ios: <NativeTabs.Trigger.Icon sf="calendar" />,
android: (
<NativeTabs.Trigger.Icon
src={<VectorIcon family={Ionicons as VectorIconFamily} name="calendar-outline" />}
selectedColor={SELECTED_COLOR}
/>
),
})}
<NativeTabs.Trigger.Label>Bookings</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="(availability)">
<NativeTabs.Trigger.Icon
sf={{ default: "clock", selected: "clock.fill" }}
src={<VectorIcon family={MaterialCommunityIcons} name="clock" />}
/>
{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={SELECTED_COLOR}
/>
),
})}
<NativeTabs.Trigger.Label>Availability</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="(more)">
<NativeTabs.Trigger.Icon
sf={{ default: "ellipsis", selected: "ellipsis" }}
src={<VectorIcon family={MaterialCommunityIcons} name="dots-horizontal" />}
/>
{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={SELECTED_COLOR}
/>
),
})}
<NativeTabs.Trigger.Label>More</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
</NativeTabs>
@@ -22,6 +22,34 @@ export const EventTypeListItem = ({
useEventTypeListItemData(item);
const isLast = index === filteredEventTypes.length - 1;
// Calculate badge count to force remount when badges change
// This fixes SwiftUI Host caching stale layout measurements
const hasSeats =
item.seats &&
!("disabled" in item.seats && item.seats.disabled) &&
"seatsPerTimeSlot" in item.seats &&
item.seats.seatsPerTimeSlot &&
item.seats.seatsPerTimeSlot > 0;
const hasRecurrence =
item.recurrence &&
!("disabled" in item.recurrence && item.recurrence.disabled) &&
"occurrences" in item.recurrence &&
item.recurrence.occurrences;
const requiresConfirmation =
item.confirmationPolicy &&
!("disabled" in item.confirmationPolicy && item.confirmationPolicy.disabled) &&
"type" in item.confirmationPolicy &&
item.confirmationPolicy.type === "always";
const badgeCount = [
true, // duration always present
item.hidden,
hasSeats,
hasPrice,
hasRecurrence,
requiresConfirmation,
].filter(Boolean).length;
type ButtonSystemImage = React.ComponentProps<typeof Button>["systemImage"];
type EventTypeIcon = Exclude<ButtonSystemImage, undefined>;
@@ -63,6 +91,8 @@ export const EventTypeListItem = ({
},
];
// Use minHeight based on badge count to ensure proper spacing
// This fixes SwiftUI Host caching stale layout measurements
return (
<View
className={`bg-cal-bg active:bg-cal-bg-secondary ${!isLast ? "border-b border-cal-border" : ""}`}
@@ -85,13 +115,20 @@ export const EventTypeListItem = ({
))}
</ContextMenu.Items>
<ContextMenu.Trigger>
<View className="flex-row items-center justify-between">
{/* Calculate minHeight based on badge rows to ensure proper spacing */}
{/* 1-3 badges = 1 row, 4-5 badges = likely 2 rows, 6 badges = 2 rows */}
<View
className="flex-row items-start justify-between"
style={{
paddingVertical: 16,
minHeight: badgeCount <= 3 ? 100 : badgeCount <= 5 ? 130 : 160,
}}
>
<Pressable
onPress={() => handleEventTypePress(item)}
style={{
paddingTop: 16,
paddingBottom: 22,
paddingLeft: 16,
paddingBottom: 10,
flex: 1,
marginRight: 12,
}}
@@ -114,7 +151,7 @@ export const EventTypeListItem = ({
/>
</Pressable>
<View style={{ paddingRight: 16, flexShrink: 0 }}>
<View style={{ paddingRight: 16, paddingTop: 4, flexShrink: 0 }}>
<Host matchContents>
<ContextMenu
modifiers={[buttonStyle(isLiquidGlassAvailable() ? "glass" : "bordered")]}
+7
View File
@@ -7,6 +7,7 @@ import {
} from "@/services/oauthService";
import type { UserProfile } from "@/services/types/users.types";
import { WebAuthService } from "@/services/webAuth";
import { clearQueryCache } from "@/utils/queryPersister";
import { secureStorage } from "@/utils/storage";
/**
@@ -143,6 +144,12 @@ export function AuthProvider({ children }: AuthProviderProps) {
const logout = useCallback(async () => {
try {
await clearAuth();
// Clear all cached queries to ensure fresh data on re-login
try {
await clearQueryCache();
} catch (cacheError) {
console.warn("Failed to clear query cache during logout:", cacheError);
}
resetAuthState();
} catch (error) {
const message = getErrorMessage(error);
+1 -1
View File
@@ -221,7 +221,7 @@ export class CalComOAuthService {
authUrl: string
): Promise<{ type: "success"; params: Record<string, string> } | { type: "error" }> {
const result = await WebBrowser.openAuthSessionAsync(authUrl, this.config.redirectUri, {
preferEphemeralSession: false,
preferEphemeralSession: true,
});
if (result.type === "success") {