Skip to content

Commit 18017b5

Browse files
authored
feat(companion): TypeScript best practices improvements (#26206)
* feat(companion): TypeScript best practices improvements - Add separate tsconfig.extension.json for extension code typechecking - Add typecheck scripts: typecheck, typecheck:extension, typecheck:all - Create ambient declarations for browser extension APIs (browser.d.ts) - Create type definitions for attendee and google-calendar types - Replace all @ts-ignore with @ts-expect-error or remove where ambient declarations cover them - Replace any types with proper types in AuthContext, webAuth, buildPartialUpdatePayload - Fix implicit any errors in extension content.ts - Update deepEqual function to use unknown instead of any for better type safety All typechecks now pass for both app and extension code. * feat(companion): enable full TypeScript strict mode (#26208) * feat(companion): enable strictNullChecks and strictFunctionTypes Phase 1 of strict mode enablement for the companion app. Changes: - Enable strictNullChecks and strictFunctionTypes in tsconfig.json - Fix null check for scheduleDetails in fetchScheduleDetails - Add explicit type annotations to limits arrays in event-type-detail.tsx - Filter conferencing options with null appId before passing to buildLocationOptions - Update LimitsTab interface to use union type for field parameter - Add optional chaining for onEdit, onDuplicate, onDelete in EventTypeListItem.ios.tsx - Add explicit type annotations to options arrays in AvailabilityListScreen.tsx - Add explicit type annotations to dateOptions and timeOptions in RescheduleScreen.tsx - Fix type guard return types in calcom.ts to explicitly return boolean All typechecks pass for both app and extension code. * feat(companion): enable full strict mode and fix any types (Phase 2 + Step 7) Phase 2: Enable full strict mode - Replace strictNullChecks + strictFunctionTypes with strict: true - Fix implicit any errors in Alert.prompt callbacks (BookingDetailScreen, useBookingActions) Step 7: Fix metadata any types in type definition files - Update UserProfile.metadata to Record<string, unknown> - Update CreateEventTypeInput.metadata to Record<string, unknown> - Update Booking.responses to Record<string, unknown> All typechecks pass for both app and extension code with full strict mode enabled. * use JSON.stringify() * fix restores the recursive key comparison with sorted keys * prevents false positives when objects have different keys * feat(companion): add tree shaking optimization scripts (#26212) * feat(companion): add tree shaking optimization scripts Add npm scripts to enable Expo's experimental tree shaking for production builds: - export: Basic expo export command - export:ios / export:android: Platform-specific exports - export:optimized: Export with tree shaking enabled (all platforms) - export:optimized:ios / export:optimized:android: Platform-specific with tree shaking Bundle size improvement measured: - Baseline: 5.48 MB (1804 modules) - With tree shaking: 5.25 MB (1831 modules) - Reduction: ~230 KB (4.2% smaller) Also documented tree shaking configuration options in .env.example for: - npm scripts (recommended) - Manual env var setting - EAS build profile configuration * revert .env.example file * revert .env.example file
1 parent ab512ea commit 18017b5

26 files changed

+243
-74
lines changed

companion/app/event-type-detail.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,11 @@ export default function EventTypeDetail() {
222222
];
223223

224224
const getLocationOptionsForDropdown = (): LocationOptionGroup[] => {
225-
return buildLocationOptions(conferencingOptions);
225+
// Filter out conferencing options with null appId
226+
const validOptions = conferencingOptions.filter(
227+
(opt): opt is ConferencingOption & { appId: string } => opt.appId !== null
228+
);
229+
return buildLocationOptions(validOptions);
226230
};
227231

228232
const handleAddLocation = (location: LocationItem) => {
@@ -333,7 +337,7 @@ export default function EventTypeDetail() {
333337
setScheduleDetailsLoading(true);
334338
const scheduleDetails = await CalComAPIService.getScheduleById(scheduleId);
335339
setSelectedScheduleDetails(scheduleDetails);
336-
if (scheduleDetails.timeZone) {
340+
if (scheduleDetails?.timeZone) {
337341
setSelectedTimezone(scheduleDetails.timeZone);
338342
}
339343
} catch (error) {
@@ -421,7 +425,7 @@ export default function EventTypeDetail() {
421425
// Load booking frequency limits
422426
if (eventType.bookingLimitsCount && !("disabled" in eventType.bookingLimitsCount)) {
423427
setLimitBookingFrequency(true);
424-
const limits = [];
428+
const limits: Array<{ id: number; value: string; unit: string }> = [];
425429
let idCounter = 1;
426430
if (eventType.bookingLimitsCount.day) {
427431
limits.push({
@@ -459,7 +463,7 @@ export default function EventTypeDetail() {
459463
// Load duration limits
460464
if (eventType.bookingLimitsDuration && !("disabled" in eventType.bookingLimitsDuration)) {
461465
setLimitTotalDuration(true);
462-
const limits = [];
466+
const limits: Array<{ id: number; value: string; unit: string }> = [];
463467
let idCounter = 1;
464468
if (eventType.bookingLimitsDuration.day) {
465469
limits.push({

companion/components/event-type-detail/tabs/LimitsTab.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ interface LimitsTabProps {
3636
toggleBookingFrequency: (value: boolean) => void;
3737
frequencyAnimationValue: Animated.Value;
3838
frequencyLimits: FrequencyLimit[];
39-
updateFrequencyLimit: (id: number, field: string, value: string) => void;
39+
updateFrequencyLimit: (id: number, field: "value" | "unit", value: string) => void;
4040
setShowFrequencyUnitDropdown: (id: number) => void;
4141
removeFrequencyLimit: (id: number) => void;
4242
addFrequencyLimit: () => void;
@@ -50,7 +50,7 @@ interface LimitsTabProps {
5050
toggleTotalDuration: (value: boolean) => void;
5151
durationAnimationValue: Animated.Value;
5252
durationLimits: DurationLimit[];
53-
updateDurationLimit: (id: number, field: string, value: string) => void;
53+
updateDurationLimit: (id: number, field: "value" | "unit", value: string) => void;
5454
setShowDurationUnitDropdown: (id: number) => void;
5555
removeDurationLimit: (id: number) => void;
5656
addDurationLimit: () => void;

companion/components/event-type-detail/utils/buildPartialUpdatePayload.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type { EventType } from "../../../services/calcom";
22
import type { LocationItem } from "../../../types/locations";
3-
import { mapItemToApiLocation } from "../../../utils/locationHelpers";
43
import {
54
parseBufferTime,
65
parseMinimumNotice,
76
parseFrequencyUnit,
87
parseSlotInterval,
98
} from "../../../utils/eventTypeParsers";
9+
import { mapItemToApiLocation } from "../../../utils/locationHelpers";
1010

1111
interface FrequencyLimit {
1212
id: number;
@@ -105,7 +105,7 @@ function hasMultipleDurationsChanged(
105105
const currentDurations = selectedDurations.map(parseDurationString).sort((a, b) => a - b);
106106
const originalDurations = [...originalOptions].sort((a: number, b: number) => a - b);
107107

108-
if (!deepEqual(currentDurations, originalDurations)) return true;
108+
if (!areEqual(currentDurations, originalDurations)) return true;
109109

110110
// Also check if the default (main) duration changed
111111
const currentDefault = parseDurationString(defaultDuration || mainDuration);
@@ -114,24 +114,22 @@ function hasMultipleDurationsChanged(
114114
return currentDefault !== originalDefault;
115115
}
116116

117-
function deepEqual(a: any, b: any): boolean {
117+
function areEqual(a: unknown, b: unknown): boolean {
118118
if (a === b) return true;
119-
if (a == null || b == null) return a == b;
120-
if (typeof a !== typeof b) return false;
119+
if (a == null || b == null) return false;
120+
if (typeof a !== "object" || typeof b !== "object") return false;
121121

122122
if (Array.isArray(a) && Array.isArray(b)) {
123123
if (a.length !== b.length) return false;
124-
return a.every((item, index) => deepEqual(item, b[index]));
125-
}
126-
127-
if (typeof a === "object" && typeof b === "object") {
128-
const keysA = Object.keys(a);
129-
const keysB = Object.keys(b);
130-
if (keysA.length !== keysB.length) return false;
131-
return keysA.every((key) => deepEqual(a[key], b[key]));
124+
return a.every((item, index) => areEqual(item, b[index]));
132125
}
133126

134-
return false;
127+
const objA = a as Record<string, unknown>;
128+
const objB = b as Record<string, unknown>;
129+
const keysA = Object.keys(objA).sort();
130+
const keysB = Object.keys(objB).sort();
131+
if (keysA.length !== keysB.length) return false;
132+
return keysA.every((key, index) => key === keysB[index] && areEqual(objA[key], objB[key]));
135133
}
136134

137135
function normalizeLocation(loc: any): any {
@@ -172,7 +170,7 @@ function haveLocationsChanged(
172170
currentMapped.sort(sortByType);
173171
originalMapped.sort(sortByType);
174172

175-
return !deepEqual(currentMapped, originalMapped);
173+
return !areEqual(currentMapped, originalMapped);
176174
}
177175

178176
function hasBookingLimitsCountChanged(
@@ -207,7 +205,7 @@ function hasBookingLimitsCountChanged(
207205
});
208206
}
209207

210-
return !deepEqual(currentLimits, originalLimits);
208+
return !areEqual(currentLimits, originalLimits);
211209
}
212210

213211
function hasBookingLimitsDurationChanged(
@@ -242,7 +240,7 @@ function hasBookingLimitsDurationChanged(
242240
});
243241
}
244242

245-
return !deepEqual(currentLimits, originalLimits);
243+
return !areEqual(currentLimits, originalLimits);
246244
}
247245

248246
function hasBookingWindowChanged(
@@ -368,7 +366,7 @@ function hasBookerLayoutsChanged(
368366
const currentNormalized = selectedLayouts.map(mapLayoutToApi).sort();
369367
const originalNormalized = originalEnabled.map((l: string) => mapLayoutToApi(l)).sort();
370368

371-
if (!deepEqual(currentNormalized, originalNormalized)) return true;
369+
if (!areEqual(currentNormalized, originalNormalized)) return true;
372370
return mapLayoutToApi(defaultLayout) !== mapLayoutToApi(originalDefault || "");
373371
}
374372

companion/components/event-type-list-item/EventTypeListItem.ios.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,19 @@ export const EventTypeListItem = ({
4949
{
5050
label: "Edit",
5151
icon: "pencil",
52-
onPress: () => onEdit(item),
52+
onPress: () => onEdit?.(item),
5353
role: "default",
5454
},
5555
{
5656
label: "Duplicate",
5757
icon: "square.on.square",
58-
onPress: () => onDuplicate(item),
58+
onPress: () => onDuplicate?.(item),
5959
role: "default",
6060
},
6161
{
6262
label: "Delete",
6363
icon: "trash",
64-
onPress: () => onDelete(item),
64+
onPress: () => onDelete?.(item),
6565
role: "destructive",
6666
},
6767
];

companion/components/screens/AvailabilityListScreen.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,11 @@ export function AvailabilityListScreen({
9797
const handleScheduleLongPress = (schedule: Schedule) => {
9898
if (Platform.OS !== "ios") {
9999
// Fallback for non-iOS platforms (Android Alert supports max 3 buttons)
100-
const options = [];
100+
const options: Array<{
101+
text: string;
102+
onPress: () => void;
103+
style?: "destructive" | "cancel" | "default";
104+
}> = [];
101105
if (!schedule.isDefault) {
102106
options.push({ text: "Set as default", onPress: () => handleSetAsDefault(schedule) });
103107
}

companion/components/screens/BookingDetailScreen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
260260
{
261261
text: "Cancel Booking",
262262
style: "destructive",
263-
onPress: (reason) => {
263+
onPress: (reason?: string) => {
264264
performCancelBooking(reason?.trim() || "Cancelled by host");
265265
},
266266
},

companion/components/screens/RescheduleScreen.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
122122

123123
// Generate date options for picker (next 90 days)
124124
const dateOptions = React.useMemo(() => {
125-
const options = [];
125+
const options: Array<{ label: string; value: Date }> = [];
126126
const today = new Date();
127127
for (let i = 0; i < 90; i++) {
128128
const date = new Date(today);
@@ -141,7 +141,7 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
141141

142142
// Generate time options (every 15 minutes)
143143
const timeOptions = React.useMemo(() => {
144-
const options = [];
144+
const options: Array<{ label: string; value: { hour: number; minute: number } }> = [];
145145
for (let hour = 0; hour < 24; hour++) {
146146
for (let minute = 0; minute < 60; minute += 15) {
147147
const time = `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;

companion/contexts/AuthContext.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,29 @@ import {
44
OAuthTokens,
55
CalComOAuthService,
66
} from "../services/oauthService";
7+
import type { UserProfile } from "../services/types/users.types";
78
import { WebAuthService } from "../services/webAuth";
89
import { secureStorage } from "../utils/storage";
910
import React, { createContext, useContext, useState, useEffect, ReactNode } from "react";
1011

12+
/**
13+
* Simplified user info stored in auth context
14+
* Contains only the essential fields needed for the app
15+
*/
16+
interface AuthUserInfo {
17+
id: number;
18+
email: string;
19+
name: string;
20+
username: string;
21+
}
22+
1123
interface AuthContextType {
1224
isAuthenticated: boolean;
1325
accessToken: string | null;
1426
refreshToken: string | null;
15-
userInfo: any;
27+
userInfo: AuthUserInfo | null;
1628
isWebSession: boolean;
17-
loginFromWebSession: (userInfo: any) => Promise<void>;
29+
loginFromWebSession: (userInfo: UserProfile) => Promise<void>;
1830
loginWithOAuth: () => Promise<void>;
1931
logout: () => Promise<void>;
2032
loading: boolean;
@@ -44,7 +56,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
4456
const [isAuthenticated, setIsAuthenticated] = useState(false);
4557
const [accessToken, setAccessToken] = useState<string | null>(null);
4658
const [refreshToken, setRefreshToken] = useState<string | null>(null);
47-
const [userInfo, setUserInfo] = useState<any>(null);
59+
const [userInfo, setUserInfo] = useState<AuthUserInfo | null>(null);
4860
const [isWebSession, setIsWebSession] = useState(false);
4961
const [loading, setLoading] = useState(true);
5062
const [oauthService] = useState(() => {
@@ -237,9 +249,14 @@ export function AuthProvider({ children }: AuthProviderProps) {
237249
}
238250
};
239251

240-
const loginFromWebSession = async (sessionUserInfo: any) => {
252+
const loginFromWebSession = async (sessionUserInfo: UserProfile) => {
241253
try {
242-
setUserInfo(sessionUserInfo);
254+
setUserInfo({
255+
id: sessionUserInfo.id,
256+
email: sessionUserInfo.email,
257+
name: sessionUserInfo.name,
258+
username: sessionUserInfo.username,
259+
});
243260
setIsAuthenticated(true);
244261
setIsWebSession(true);
245262
await storage.set(AUTH_TYPE_KEY, "web_session");

companion/extension/browser.d.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Ambient declarations for browser extension APIs
3+
* These provide proper typing for Firefox/Safari browser namespace
4+
* and Brave-specific navigator properties
5+
*/
6+
7+
// Firefox/Safari use 'browser' namespace instead of 'chrome'
8+
// This is a WebExtension API that mirrors chrome.* APIs
9+
declare const browser: typeof chrome | undefined;
10+
11+
// Brave adds isBrave() to navigator
12+
interface Navigator {
13+
brave?: {
14+
isBrave: () => Promise<boolean>;
15+
};
16+
}
17+
18+
// WXT global function for defining background scripts
19+
declare function defineBackground(fn: () => void): void;

companion/extension/entrypoints/background/index.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ function detectBrowser(): BrowserType {
3535
const userAgent = navigator.userAgent.toLowerCase();
3636

3737
// Check for Brave
38-
// @ts-ignore - Brave adds this to navigator
3938
if (navigator.brave && typeof navigator.brave.isBrave === "function") {
4039
return BrowserType.Brave;
4140
}
@@ -95,9 +94,7 @@ function getBrowserDisplayName(): string {
9594

9695
// Get the appropriate browser API namespace
9796
function getBrowserAPI(): typeof chrome {
98-
// @ts-ignore - Firefox/Safari use browser namespace
99-
if (typeof browser !== "undefined" && browser.runtime) {
100-
// @ts-ignore
97+
if (typeof browser !== "undefined" && browser?.runtime) {
10198
return browser;
10299
}
103100
return chrome;
@@ -130,8 +127,7 @@ function getTabsAPI(): typeof chrome.tabs | null {
130127
// Get action API with cross-browser support
131128
function getActionAPI(): typeof chrome.action | null {
132129
const api = getBrowserAPI();
133-
// @ts-ignore - Some browsers use browserAction instead of action
134-
return api?.action || api?.browserAction || null;
130+
return api?.action || null;
135131
}
136132

137133
// Check if the URL is a restricted page where content scripts can't run
@@ -168,7 +164,6 @@ function openAppPage(): void {
168164
}
169165
}
170166

171-
// @ts-ignore - WXT provides this globally
172167
export default defineBackground(() => {
173168
const browserType = detectBrowser();
174169
const browserName = getBrowserDisplayName();
@@ -377,8 +372,10 @@ async function handleExtensionOAuth(authUrl: string): Promise<string> {
377372
// Firefox and Safari use Promise-based API
378373
if (browserType === BrowserType.Firefox || browserType === BrowserType.Safari) {
379374
try {
380-
// @ts-ignore - Firefox/Safari return Promises
381-
const result = identityAPI.launchWebAuthFlow({ url: authUrl, interactive: true });
375+
const result = identityAPI.launchWebAuthFlow({
376+
url: authUrl,
377+
interactive: true,
378+
}) as Promise<string | undefined> | void;
382379

383380
if (result && typeof result.then === "function") {
384381
result

0 commit comments

Comments
 (0)