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:
@@ -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"
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user