Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion companion/app/(tabs)/(more)/index.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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);
Expand Down Expand Up @@ -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"
Expand Down
6 changes: 5 additions & 1 deletion companion/app/(tabs)/(more)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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);
Expand Down Expand Up @@ -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 }}
Expand Down
71 changes: 51 additions & 20 deletions companion/app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,82 @@
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 />;
}

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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>;

Expand Down Expand Up @@ -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" : ""}`}
Expand All @@ -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,
}}
Expand All @@ -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")]}
Expand Down
7 changes: 7 additions & 0 deletions companion/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion companion/services/oauthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
Loading