Skip to content

Commit fa53f54

Browse files
authored
feat(companion): add configurable landing page (calcom#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
1 parent f07bed1 commit fa53f54

16 files changed

Lines changed: 555 additions & 75 deletions

File tree

companion/app.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"name": "Calcom",
44
"slug": "calcom-companion",
55
"scheme": "calcom",
6-
"version": "1.0.3",
6+
"version": "1.0.4",
77
"orientation": "portrait",
88
"icon": "./assets/icon.png",
99
"userInterfaceStyle": "automatic",
@@ -102,7 +102,7 @@
102102
"minWidth": "180dp",
103103
"minHeight": "110dp",
104104
"description": "View your upcoming Cal.com bookings",
105-
"previewImage": "./assets/icon.png",
105+
"previewImage": "./assets/widget-preview.png",
106106
"resizeMode": "horizontal|vertical",
107107
"targetCellWidth": 3,
108108
"targetCellHeight": 2

companion/app/(tabs)/(availability)/index.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,10 @@ export default function Availability() {
100100
>
101101
<Stack.Header.Title large>Availability</Stack.Header.Title>
102102
<Stack.Header.Right>
103-
{/* New Menu */}
104-
<Stack.Header.Menu>
103+
{/* New Button - directly opens create dialog */}
104+
<Stack.Header.Button onPress={handleCreateNew}>
105105
<Stack.Header.Icon sf="plus" />
106-
107-
<Stack.Header.MenuAction icon="clock" onPress={handleCreateNew}>
108-
New Availability
109-
</Stack.Header.MenuAction>
110-
</Stack.Header.Menu>
106+
</Stack.Header.Button>
111107

112108
{/* Profile Button */}
113109
{userProfile?.avatarUrl ? (

companion/app/(tabs)/(bookings)/index.ios.tsx

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,58 @@
11
import type { NativeStackHeaderItemMenuAction } from "@react-navigation/native-stack";
22
import { isLiquidGlassAvailable } from "expo-glass-effect";
3-
import { Stack } from "expo-router";
4-
import { useState } from "react";
3+
import { Stack, useLocalSearchParams } from "expo-router";
4+
import { useState, useEffect, useRef } from "react";
55
import { useColorScheme } from "react-native";
66

77
import { BookingListScreen } from "@/components/booking-list-screen/BookingListScreen";
88
import { useEventTypes } from "@/hooks";
9-
import { useActiveBookingFilter } from "@/hooks/useActiveBookingFilter";
9+
import { type BookingFilter, useActiveBookingFilter } from "@/hooks/useActiveBookingFilter";
10+
11+
const VALID_FILTERS: BookingFilter[] = [
12+
"upcoming",
13+
"unconfirmed",
14+
"recurring",
15+
"past",
16+
"cancelled",
17+
];
18+
19+
function isValidBookingFilter(value: string | undefined): value is BookingFilter {
20+
return value !== undefined && VALID_FILTERS.includes(value as BookingFilter);
21+
}
1022

1123
export default function Bookings() {
24+
const { filter } = useLocalSearchParams<{ filter?: string }>();
25+
const initialFilter = isValidBookingFilter(filter) ? filter : "upcoming";
26+
1227
const [searchQuery, setSearchQuery] = useState("");
1328
const [selectedEventTypeId, setSelectedEventTypeId] = useState<number | null>(null);
1429
const { data: eventTypes } = useEventTypes();
1530

1631
// Use the active booking filter hook
1732
const { activeFilter, filterOptions, filterParams, handleFilterChange } = useActiveBookingFilter(
18-
"upcoming",
33+
initialFilter,
1934
() => {
2035
// Clear dependent filters when status filter changes
2136
setSearchQuery("");
2237
setSelectedEventTypeId(null);
2338
}
2439
);
2540

41+
// Track if we want to ignore the next activeFilter change (because it came from URL sync)
42+
const lastUrlFilter = useRef<string | null>(null);
43+
44+
// Update filter when URL params change (for when component is already mounted)
45+
useEffect(() => {
46+
if (
47+
isValidBookingFilter(filter) &&
48+
filter !== activeFilter &&
49+
filter !== lastUrlFilter.current
50+
) {
51+
lastUrlFilter.current = filter;
52+
handleFilterChange(filter);
53+
}
54+
}, [filter, activeFilter, handleFilterChange]);
55+
2656
const colorScheme = useColorScheme();
2757
const isDark = colorScheme === "dark";
2858
const textColor = isDark ? "#FFFFFF" : "#000000";

companion/app/(tabs)/(bookings)/index.tsx

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Ionicons } from "@expo/vector-icons";
2-
import { useState } from "react";
2+
import { useLocalSearchParams } from "expo-router";
3+
import { useState, useEffect, useRef } from "react";
34
import { Text, TextInput, useColorScheme, View } from "react-native";
45
import { BookingListScreen } from "@/components/booking-list-screen/BookingListScreen";
56
import { Header } from "@/components/Header";
@@ -10,11 +11,26 @@ import {
1011
DropdownMenuTrigger,
1112
} from "@/components/ui/dropdown-menu";
1213
import { AppPressable } from "@/components/AppPressable";
13-
import { useActiveBookingFilter } from "@/hooks/useActiveBookingFilter";
14+
import { type BookingFilter, useActiveBookingFilter } from "@/hooks/useActiveBookingFilter";
1415
import { useEventTypes } from "@/hooks";
1516
import { getColors } from "@/constants/colors";
1617

18+
const VALID_FILTERS: BookingFilter[] = [
19+
"upcoming",
20+
"unconfirmed",
21+
"recurring",
22+
"past",
23+
"cancelled",
24+
];
25+
26+
function isValidBookingFilter(value: string | undefined): value is BookingFilter {
27+
return value !== undefined && VALID_FILTERS.includes(value as BookingFilter);
28+
}
29+
1730
export default function Bookings() {
31+
const { filter } = useLocalSearchParams<{ filter?: string }>();
32+
const initialFilter = isValidBookingFilter(filter) ? filter : "upcoming";
33+
1834
const [searchQuery, setSearchQuery] = useState("");
1935
const [selectedEventTypeId, setSelectedEventTypeId] = useState<number | null>(null);
2036
const [selectedEventTypeLabel, setSelectedEventTypeLabel] = useState<string | null>(null);
@@ -26,15 +42,37 @@ export default function Bookings() {
2642
const { data: eventTypes = [], isLoading: eventTypesLoading } = useEventTypes();
2743

2844
// Use the active booking filter hook
29-
const { activeFilter, filterOptions, filterParams, handleFilterChange } = useActiveBookingFilter(
30-
"upcoming",
31-
() => {
32-
// Clear dependent filters when status filter changes
33-
setSearchQuery("");
34-
setSelectedEventTypeId(null);
35-
setSelectedEventTypeLabel(null);
45+
const {
46+
activeFilter,
47+
filterOptions,
48+
filterParams,
49+
handleFilterChange: originalHandleFilterChange,
50+
} = useActiveBookingFilter(initialFilter, () => {
51+
// Clear dependent filters when status filter changes
52+
setSearchQuery("");
53+
setSelectedEventTypeId(null);
54+
setSelectedEventTypeLabel(null);
55+
});
56+
57+
// Wrap handleFilterChange with debug logging
58+
const handleFilterChange = (filter: string) => {
59+
originalHandleFilterChange(filter as BookingFilter);
60+
};
61+
62+
// Track if we want to ignore the next activeFilter change (because it came from URL sync)
63+
const lastUrlFilter = useRef<string | null>(null);
64+
65+
// Reactively update filter when URL params change (e.g., from deep link)
66+
useEffect(() => {
67+
if (
68+
isValidBookingFilter(filter) &&
69+
filter !== activeFilter &&
70+
filter !== lastUrlFilter.current
71+
) {
72+
lastUrlFilter.current = filter;
73+
originalHandleFilterChange(filter);
3674
}
37-
);
75+
}, [filter, activeFilter, originalHandleFilterChange]);
3876

3977
const handleSearch = (query: string) => {
4078
setSearchQuery(query);

companion/app/(tabs)/(more)/index.ios.tsx

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ import {
1212
useColorScheme,
1313
View,
1414
} from "react-native";
15+
import { LandingPagePicker } from "@/components/LandingPagePicker";
1516
import { LogoutConfirmModal } from "@/components/LogoutConfirmModal";
1617
import { useAuth } from "@/contexts/AuthContext";
1718
import { useQueryContext } from "@/contexts/QueryContext";
1819
import { useUserProfile } from "@/hooks";
19-
import { showErrorAlert, showNotAvailableAlert } from "@/utils/alerts";
20+
import { type LandingPage, useUserPreferences } from "@/hooks/useUserPreferences";
21+
import { showErrorAlert, showSuccessAlert, showNotAvailableAlert } from "@/utils/alerts";
2022
import { openInAppBrowser } from "@/utils/browser";
2123
import { getAvatarUrl } from "@/utils/getAvatarUrl";
2224
import { getColors } from "@/constants/colors";
@@ -33,7 +35,9 @@ export default function More() {
3335
const router = useRouter();
3436
const { logout } = useAuth();
3537
const { clearCache } = useQueryContext();
38+
const { preferences, setLandingPage, landingPageLabel } = useUserPreferences();
3639
const [showLogoutModal, setShowLogoutModal] = useState(false);
40+
const [showLandingPagePicker, setShowLandingPagePicker] = useState(false);
3741
const { data: userProfile } = useUserProfile();
3842
const colorScheme = useColorScheme();
3943
const isDark = colorScheme === "dark";
@@ -48,6 +52,15 @@ export default function More() {
4852
border: isDark ? "#4D4D4D" : "#E5E5EA",
4953
};
5054

55+
const handleLandingPageSelect = async (value: LandingPage) => {
56+
try {
57+
await setLandingPage(value);
58+
showSuccessAlert("Saved", "First page updated");
59+
} catch {
60+
showErrorAlert("Error", "Failed to save preference. Please try again.");
61+
}
62+
};
63+
5164
const performLogout = async () => {
5265
try {
5366
// Clear in-memory cache before logout
@@ -183,6 +196,74 @@ export default function More() {
183196
))}
184197
</View>
185198

199+
{/* App Settings */}
200+
<View
201+
style={{
202+
marginTop: 24,
203+
borderRadius: 8,
204+
borderWidth: 1,
205+
borderColor: colors.border,
206+
backgroundColor: colors.backgroundSecondary,
207+
overflow: "hidden",
208+
}}
209+
>
210+
<View
211+
style={{
212+
borderBottomWidth: 1,
213+
borderBottomColor: colors.border,
214+
backgroundColor: isDark ? "#2C2C2E" : "#F9FAFB",
215+
paddingHorizontal: 16,
216+
paddingVertical: 8,
217+
}}
218+
>
219+
<Text
220+
style={{ color: isDark ? "#A3A3A3" : "#6B7280" }}
221+
className="text-xs font-semibold uppercase"
222+
>
223+
App Settings
224+
</Text>
225+
</View>
226+
<TouchableOpacity
227+
onPress={() => setShowLandingPagePicker(true)}
228+
style={{
229+
flexDirection: "row",
230+
alignItems: "center",
231+
justifyContent: "space-between",
232+
backgroundColor: colors.backgroundSecondary,
233+
paddingHorizontal: 20,
234+
paddingVertical: 16,
235+
}}
236+
activeOpacity={0.7}
237+
>
238+
<View className="flex-row items-center">
239+
<Ionicons name="home-outline" size={20} color={colors.text} />
240+
<Text style={{ color: colors.text }} className="ml-3 text-base font-semibold">
241+
First Page
242+
</Text>
243+
</View>
244+
<View className="flex-row items-center">
245+
<View
246+
style={{
247+
marginLeft: 12,
248+
marginRight: 8,
249+
borderRadius: 6,
250+
backgroundColor: isDark ? "#3A3A3C" : "#F3F4F6",
251+
paddingHorizontal: 10,
252+
paddingVertical: 4,
253+
}}
254+
>
255+
<Text
256+
style={{ color: isDark ? "#A3A3A3" : "#4B5563" }}
257+
className="text-sm font-medium"
258+
>
259+
{landingPageLabel}
260+
</Text>
261+
</View>
262+
<Ionicons name="chevron-forward" size={20} color={colors.textSecondary} />
263+
</View>
264+
</TouchableOpacity>
265+
</View>
266+
186267
{/* Delete Account Link */}
187268
<View
188269
style={{
@@ -268,6 +349,14 @@ export default function More() {
268349
}}
269350
onCancel={() => setShowLogoutModal(false)}
270351
/>
352+
353+
{/* Landing Page Picker */}
354+
<LandingPagePicker
355+
visible={showLandingPagePicker}
356+
currentValue={preferences.landingPage}
357+
onSelect={handleLandingPageSelect}
358+
onClose={() => setShowLandingPagePicker(false)}
359+
/>
271360
</>
272361
);
273362
}

0 commit comments

Comments
 (0)