From 18017b5320a7451c5df528f85fcc731e665f3ecb Mon Sep 17 00:00:00 2001 From: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com> Date: Fri, 26 Dec 2025 22:43:50 +0530 Subject: [PATCH] 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 - Update CreateEventTypeInput.metadata to Record - Update Booking.responses to Record 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 --- companion/app/event-type-detail.tsx | 12 ++-- .../event-type-detail/tabs/LimitsTab.tsx | 4 +- .../utils/buildPartialUpdatePayload.ts | 34 +++++----- .../EventTypeListItem.ios.tsx | 6 +- .../screens/AvailabilityListScreen.tsx | 6 +- .../screens/BookingDetailScreen.tsx | 2 +- .../components/screens/RescheduleScreen.tsx | 4 +- companion/contexts/AuthContext.tsx | 27 ++++++-- companion/extension/browser.d.ts | 19 ++++++ .../extension/entrypoints/background/index.ts | 15 ++--- companion/extension/entrypoints/content.ts | 21 ++++-- companion/extension/types/google-calendar.ts | 64 +++++++++++++++++++ companion/hooks/useBookingActions.ts | 2 +- companion/package.json | 8 +++ companion/services/calcom.ts | 9 ++- companion/services/oauthService.ts | 10 ++- companion/services/types/bookings.types.ts | 2 +- companion/services/types/event-types.types.ts | 2 +- companion/services/types/users.types.ts | 2 +- companion/services/webAuth.ts | 4 +- companion/tsconfig.extension.json | 18 ++++++ companion/tsconfig.json | 3 +- companion/types/attendee.ts | 7 ++ companion/types/browser.d.ts | 29 +++++++++ companion/utils/shadows.ts | 3 +- companion/utils/slugify.ts | 4 -- 26 files changed, 243 insertions(+), 74 deletions(-) create mode 100644 companion/extension/browser.d.ts create mode 100644 companion/extension/types/google-calendar.ts create mode 100644 companion/tsconfig.extension.json create mode 100644 companion/types/attendee.ts create mode 100644 companion/types/browser.d.ts diff --git a/companion/app/event-type-detail.tsx b/companion/app/event-type-detail.tsx index 702da16edd..badb2df0f6 100644 --- a/companion/app/event-type-detail.tsx +++ b/companion/app/event-type-detail.tsx @@ -222,7 +222,11 @@ export default function EventTypeDetail() { ]; const getLocationOptionsForDropdown = (): LocationOptionGroup[] => { - return buildLocationOptions(conferencingOptions); + // Filter out conferencing options with null appId + const validOptions = conferencingOptions.filter( + (opt): opt is ConferencingOption & { appId: string } => opt.appId !== null + ); + return buildLocationOptions(validOptions); }; const handleAddLocation = (location: LocationItem) => { @@ -333,7 +337,7 @@ export default function EventTypeDetail() { setScheduleDetailsLoading(true); const scheduleDetails = await CalComAPIService.getScheduleById(scheduleId); setSelectedScheduleDetails(scheduleDetails); - if (scheduleDetails.timeZone) { + if (scheduleDetails?.timeZone) { setSelectedTimezone(scheduleDetails.timeZone); } } catch (error) { @@ -421,7 +425,7 @@ export default function EventTypeDetail() { // Load booking frequency limits if (eventType.bookingLimitsCount && !("disabled" in eventType.bookingLimitsCount)) { setLimitBookingFrequency(true); - const limits = []; + const limits: Array<{ id: number; value: string; unit: string }> = []; let idCounter = 1; if (eventType.bookingLimitsCount.day) { limits.push({ @@ -459,7 +463,7 @@ export default function EventTypeDetail() { // Load duration limits if (eventType.bookingLimitsDuration && !("disabled" in eventType.bookingLimitsDuration)) { setLimitTotalDuration(true); - const limits = []; + const limits: Array<{ id: number; value: string; unit: string }> = []; let idCounter = 1; if (eventType.bookingLimitsDuration.day) { limits.push({ diff --git a/companion/components/event-type-detail/tabs/LimitsTab.tsx b/companion/components/event-type-detail/tabs/LimitsTab.tsx index 7ab42f3799..e15b43afd0 100644 --- a/companion/components/event-type-detail/tabs/LimitsTab.tsx +++ b/companion/components/event-type-detail/tabs/LimitsTab.tsx @@ -36,7 +36,7 @@ interface LimitsTabProps { toggleBookingFrequency: (value: boolean) => void; frequencyAnimationValue: Animated.Value; frequencyLimits: FrequencyLimit[]; - updateFrequencyLimit: (id: number, field: string, value: string) => void; + updateFrequencyLimit: (id: number, field: "value" | "unit", value: string) => void; setShowFrequencyUnitDropdown: (id: number) => void; removeFrequencyLimit: (id: number) => void; addFrequencyLimit: () => void; @@ -50,7 +50,7 @@ interface LimitsTabProps { toggleTotalDuration: (value: boolean) => void; durationAnimationValue: Animated.Value; durationLimits: DurationLimit[]; - updateDurationLimit: (id: number, field: string, value: string) => void; + updateDurationLimit: (id: number, field: "value" | "unit", value: string) => void; setShowDurationUnitDropdown: (id: number) => void; removeDurationLimit: (id: number) => void; addDurationLimit: () => void; diff --git a/companion/components/event-type-detail/utils/buildPartialUpdatePayload.ts b/companion/components/event-type-detail/utils/buildPartialUpdatePayload.ts index 9d0e606126..ca2be894a1 100644 --- a/companion/components/event-type-detail/utils/buildPartialUpdatePayload.ts +++ b/companion/components/event-type-detail/utils/buildPartialUpdatePayload.ts @@ -1,12 +1,12 @@ import type { EventType } from "../../../services/calcom"; import type { LocationItem } from "../../../types/locations"; -import { mapItemToApiLocation } from "../../../utils/locationHelpers"; import { parseBufferTime, parseMinimumNotice, parseFrequencyUnit, parseSlotInterval, } from "../../../utils/eventTypeParsers"; +import { mapItemToApiLocation } from "../../../utils/locationHelpers"; interface FrequencyLimit { id: number; @@ -105,7 +105,7 @@ function hasMultipleDurationsChanged( const currentDurations = selectedDurations.map(parseDurationString).sort((a, b) => a - b); const originalDurations = [...originalOptions].sort((a: number, b: number) => a - b); - if (!deepEqual(currentDurations, originalDurations)) return true; + if (!areEqual(currentDurations, originalDurations)) return true; // Also check if the default (main) duration changed const currentDefault = parseDurationString(defaultDuration || mainDuration); @@ -114,24 +114,22 @@ function hasMultipleDurationsChanged( return currentDefault !== originalDefault; } -function deepEqual(a: any, b: any): boolean { +function areEqual(a: unknown, b: unknown): boolean { if (a === b) return true; - if (a == null || b == null) return a == b; - if (typeof a !== typeof b) return false; + if (a == null || b == null) return false; + if (typeof a !== "object" || typeof b !== "object") return false; if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; - return a.every((item, index) => deepEqual(item, b[index])); + return a.every((item, index) => areEqual(item, b[index])); } - if (typeof a === "object" && typeof b === "object") { - const keysA = Object.keys(a); - const keysB = Object.keys(b); - if (keysA.length !== keysB.length) return false; - return keysA.every((key) => deepEqual(a[key], b[key])); - } - - return false; + const objA = a as Record; + const objB = b as Record; + const keysA = Object.keys(objA).sort(); + const keysB = Object.keys(objB).sort(); + if (keysA.length !== keysB.length) return false; + return keysA.every((key, index) => key === keysB[index] && areEqual(objA[key], objB[key])); } function normalizeLocation(loc: any): any { @@ -172,7 +170,7 @@ function haveLocationsChanged( currentMapped.sort(sortByType); originalMapped.sort(sortByType); - return !deepEqual(currentMapped, originalMapped); + return !areEqual(currentMapped, originalMapped); } function hasBookingLimitsCountChanged( @@ -207,7 +205,7 @@ function hasBookingLimitsCountChanged( }); } - return !deepEqual(currentLimits, originalLimits); + return !areEqual(currentLimits, originalLimits); } function hasBookingLimitsDurationChanged( @@ -242,7 +240,7 @@ function hasBookingLimitsDurationChanged( }); } - return !deepEqual(currentLimits, originalLimits); + return !areEqual(currentLimits, originalLimits); } function hasBookingWindowChanged( @@ -368,7 +366,7 @@ function hasBookerLayoutsChanged( const currentNormalized = selectedLayouts.map(mapLayoutToApi).sort(); const originalNormalized = originalEnabled.map((l: string) => mapLayoutToApi(l)).sort(); - if (!deepEqual(currentNormalized, originalNormalized)) return true; + if (!areEqual(currentNormalized, originalNormalized)) return true; return mapLayoutToApi(defaultLayout) !== mapLayoutToApi(originalDefault || ""); } diff --git a/companion/components/event-type-list-item/EventTypeListItem.ios.tsx b/companion/components/event-type-list-item/EventTypeListItem.ios.tsx index 65315449f2..a00a000d02 100644 --- a/companion/components/event-type-list-item/EventTypeListItem.ios.tsx +++ b/companion/components/event-type-list-item/EventTypeListItem.ios.tsx @@ -49,19 +49,19 @@ export const EventTypeListItem = ({ { label: "Edit", icon: "pencil", - onPress: () => onEdit(item), + onPress: () => onEdit?.(item), role: "default", }, { label: "Duplicate", icon: "square.on.square", - onPress: () => onDuplicate(item), + onPress: () => onDuplicate?.(item), role: "default", }, { label: "Delete", icon: "trash", - onPress: () => onDelete(item), + onPress: () => onDelete?.(item), role: "destructive", }, ]; diff --git a/companion/components/screens/AvailabilityListScreen.tsx b/companion/components/screens/AvailabilityListScreen.tsx index 4780f17557..9c5fa8e450 100644 --- a/companion/components/screens/AvailabilityListScreen.tsx +++ b/companion/components/screens/AvailabilityListScreen.tsx @@ -97,7 +97,11 @@ export function AvailabilityListScreen({ const handleScheduleLongPress = (schedule: Schedule) => { if (Platform.OS !== "ios") { // Fallback for non-iOS platforms (Android Alert supports max 3 buttons) - const options = []; + const options: Array<{ + text: string; + onPress: () => void; + style?: "destructive" | "cancel" | "default"; + }> = []; if (!schedule.isDefault) { options.push({ text: "Set as default", onPress: () => handleSetAsDefault(schedule) }); } diff --git a/companion/components/screens/BookingDetailScreen.tsx b/companion/components/screens/BookingDetailScreen.tsx index 855b7963f0..854c965f59 100644 --- a/companion/components/screens/BookingDetailScreen.tsx +++ b/companion/components/screens/BookingDetailScreen.tsx @@ -260,7 +260,7 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen { text: "Cancel Booking", style: "destructive", - onPress: (reason) => { + onPress: (reason?: string) => { performCancelBooking(reason?.trim() || "Cancelled by host"); }, }, diff --git a/companion/components/screens/RescheduleScreen.tsx b/companion/components/screens/RescheduleScreen.tsx index 767a43e9e8..cd445ac946 100644 --- a/companion/components/screens/RescheduleScreen.tsx +++ b/companion/components/screens/RescheduleScreen.tsx @@ -122,7 +122,7 @@ export const RescheduleScreen = forwardRef { - const options = []; + const options: Array<{ label: string; value: Date }> = []; const today = new Date(); for (let i = 0; i < 90; i++) { const date = new Date(today); @@ -141,7 +141,7 @@ export const RescheduleScreen = forwardRef { - const options = []; + const options: Array<{ label: string; value: { hour: number; minute: number } }> = []; for (let hour = 0; hour < 24; hour++) { for (let minute = 0; minute < 60; minute += 15) { const time = `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`; diff --git a/companion/contexts/AuthContext.tsx b/companion/contexts/AuthContext.tsx index e5ebcb464c..f95858e165 100644 --- a/companion/contexts/AuthContext.tsx +++ b/companion/contexts/AuthContext.tsx @@ -4,17 +4,29 @@ import { OAuthTokens, CalComOAuthService, } from "../services/oauthService"; +import type { UserProfile } from "../services/types/users.types"; import { WebAuthService } from "../services/webAuth"; import { secureStorage } from "../utils/storage"; import React, { createContext, useContext, useState, useEffect, ReactNode } from "react"; +/** + * Simplified user info stored in auth context + * Contains only the essential fields needed for the app + */ +interface AuthUserInfo { + id: number; + email: string; + name: string; + username: string; +} + interface AuthContextType { isAuthenticated: boolean; accessToken: string | null; refreshToken: string | null; - userInfo: any; + userInfo: AuthUserInfo | null; isWebSession: boolean; - loginFromWebSession: (userInfo: any) => Promise; + loginFromWebSession: (userInfo: UserProfile) => Promise; loginWithOAuth: () => Promise; logout: () => Promise; loading: boolean; @@ -44,7 +56,7 @@ export function AuthProvider({ children }: AuthProviderProps) { const [isAuthenticated, setIsAuthenticated] = useState(false); const [accessToken, setAccessToken] = useState(null); const [refreshToken, setRefreshToken] = useState(null); - const [userInfo, setUserInfo] = useState(null); + const [userInfo, setUserInfo] = useState(null); const [isWebSession, setIsWebSession] = useState(false); const [loading, setLoading] = useState(true); const [oauthService] = useState(() => { @@ -237,9 +249,14 @@ export function AuthProvider({ children }: AuthProviderProps) { } }; - const loginFromWebSession = async (sessionUserInfo: any) => { + const loginFromWebSession = async (sessionUserInfo: UserProfile) => { try { - setUserInfo(sessionUserInfo); + setUserInfo({ + id: sessionUserInfo.id, + email: sessionUserInfo.email, + name: sessionUserInfo.name, + username: sessionUserInfo.username, + }); setIsAuthenticated(true); setIsWebSession(true); await storage.set(AUTH_TYPE_KEY, "web_session"); diff --git a/companion/extension/browser.d.ts b/companion/extension/browser.d.ts new file mode 100644 index 0000000000..79098b0e4c --- /dev/null +++ b/companion/extension/browser.d.ts @@ -0,0 +1,19 @@ +/** + * Ambient declarations for browser extension APIs + * These provide proper typing for Firefox/Safari browser namespace + * and Brave-specific navigator properties + */ + +// Firefox/Safari use 'browser' namespace instead of 'chrome' +// This is a WebExtension API that mirrors chrome.* APIs +declare const browser: typeof chrome | undefined; + +// Brave adds isBrave() to navigator +interface Navigator { + brave?: { + isBrave: () => Promise; + }; +} + +// WXT global function for defining background scripts +declare function defineBackground(fn: () => void): void; diff --git a/companion/extension/entrypoints/background/index.ts b/companion/extension/entrypoints/background/index.ts index 73c8da9129..50552d69d8 100644 --- a/companion/extension/entrypoints/background/index.ts +++ b/companion/extension/entrypoints/background/index.ts @@ -35,7 +35,6 @@ function detectBrowser(): BrowserType { const userAgent = navigator.userAgent.toLowerCase(); // Check for Brave - // @ts-ignore - Brave adds this to navigator if (navigator.brave && typeof navigator.brave.isBrave === "function") { return BrowserType.Brave; } @@ -95,9 +94,7 @@ function getBrowserDisplayName(): string { // Get the appropriate browser API namespace function getBrowserAPI(): typeof chrome { - // @ts-ignore - Firefox/Safari use browser namespace - if (typeof browser !== "undefined" && browser.runtime) { - // @ts-ignore + if (typeof browser !== "undefined" && browser?.runtime) { return browser; } return chrome; @@ -130,8 +127,7 @@ function getTabsAPI(): typeof chrome.tabs | null { // Get action API with cross-browser support function getActionAPI(): typeof chrome.action | null { const api = getBrowserAPI(); - // @ts-ignore - Some browsers use browserAction instead of action - return api?.action || api?.browserAction || null; + return api?.action || null; } // Check if the URL is a restricted page where content scripts can't run @@ -168,7 +164,6 @@ function openAppPage(): void { } } -// @ts-ignore - WXT provides this globally export default defineBackground(() => { const browserType = detectBrowser(); const browserName = getBrowserDisplayName(); @@ -377,8 +372,10 @@ async function handleExtensionOAuth(authUrl: string): Promise { // Firefox and Safari use Promise-based API if (browserType === BrowserType.Firefox || browserType === BrowserType.Safari) { try { - // @ts-ignore - Firefox/Safari return Promises - const result = identityAPI.launchWebAuthFlow({ url: authUrl, interactive: true }); + const result = identityAPI.launchWebAuthFlow({ + url: authUrl, + interactive: true, + }) as Promise | void; if (result && typeof result.then === "function") { result diff --git a/companion/extension/entrypoints/content.ts b/companion/extension/entrypoints/content.ts index c23363311e..dfc212e222 100644 --- a/companion/extension/entrypoints/content.ts +++ b/companion/extension/entrypoints/content.ts @@ -726,7 +726,7 @@ export default defineContentScript({ }, 0); }); - function createTooltip(text, buttonElement) { + function createTooltip(text: string, buttonElement: HTMLElement): HTMLElement { const tooltip = document.createElement("div"); tooltip.className = "cal-tooltip"; tooltip.style.cssText = ` @@ -790,7 +790,10 @@ export default defineContentScript({ } } - async function fetchEventTypes(menu, tooltipsToCleanup) { + async function fetchEventTypes( + menu: HTMLElement, + tooltipsToCleanup: HTMLElement[] + ): Promise { try { // Check cache first const now = Date.now(); @@ -1213,7 +1216,10 @@ export default defineContentScript({ } } - function insertEventTypeLink(eventType) { + function insertEventTypeLink(eventType: { + slug: string; + users?: Array<{ username?: string }>; + }): void { // Construct the Cal.com booking link const bookingUrl = `https://cal.com/${eventType.users?.[0]?.username || "user"}/${ eventType.slug @@ -1237,7 +1243,10 @@ export default defineContentScript({ } } - function copyEventTypeLink(eventType) { + function copyEventTypeLink(eventType: { + slug: string; + users?: Array<{ username?: string }>; + }): void { // Construct the Cal.com booking link const bookingUrl = `https://cal.com/${eventType.users?.[0]?.username || "user"}/${ eventType.slug @@ -1261,7 +1270,7 @@ export default defineContentScript({ } } - function insertTextAtCursor(text) { + function insertTextAtCursor(text: string): boolean { // Find the active compose field // Gmail uses contenteditable divs for the compose body const composeBody = @@ -1314,7 +1323,7 @@ export default defineContentScript({ return true; } - function showNotification(message, type) { + function showNotification(message: string, type: "success" | "error"): void { const notification = document.createElement("div"); notification.style.cssText = ` position: fixed; diff --git a/companion/extension/types/google-calendar.ts b/companion/extension/types/google-calendar.ts new file mode 100644 index 0000000000..cecd03071a --- /dev/null +++ b/companion/extension/types/google-calendar.ts @@ -0,0 +1,64 @@ +/** + * Types for Google Calendar integration in the extension + * These types are used for parsing and handling Google Calendar data + */ + +/** + * Represents a time slot parsed from Google Calendar + */ +export interface TimeSlot { + date: string; + startTime: string; + endTime: string; + duration: number; +} + +/** + * Parsed data from Google Calendar chip elements + */ +export interface ParsedGoogleCalendarData { + title?: string; + startTime?: string; + endTime?: string; + detectedDuration?: number; + attendees?: string[]; + location?: string; + slots: TimeSlot[]; +} + +/** + * Basic event type information used in the extension + */ +export interface EventTypeBasic { + id: number; + title: string; + slug: string; + lengthInMinutes: number; +} + +/** + * Response from fetching event types + */ +export interface EventTypesResponse { + data?: EventTypeBasic[]; + error?: string; +} + +/** + * Token request for OAuth exchange + */ +export interface TokenExchangeRequest { + grant_type: string; + client_id: string; + code?: string; + redirect_uri?: string; + code_verifier?: string; + refresh_token?: string; +} + +/** + * Slots grouped by date for display + */ +export interface SlotsByDate { + [date: string]: TimeSlot[]; +} diff --git a/companion/hooks/useBookingActions.ts b/companion/hooks/useBookingActions.ts index 415eaabfb2..4b46451da8 100644 --- a/companion/hooks/useBookingActions.ts +++ b/companion/hooks/useBookingActions.ts @@ -242,7 +242,7 @@ export const useBookingActions = ({ { text: "Cancel Event", style: "destructive", - onPress: (reason) => { + onPress: (reason?: string) => { const cancellationReason = reason?.trim() || "Event cancelled by host"; cancelMutation( { uid: booking.uid, reason: cancellationReason }, diff --git a/companion/package.json b/companion/package.json index aaa0266e90..728b570db4 100644 --- a/companion/package.json +++ b/companion/package.json @@ -9,6 +9,14 @@ "ios": "expo run:ios", "web": "expo start --web", "typecheck": "tsc -p tsconfig.json --noEmit", + "typecheck:extension": "tsc -p tsconfig.extension.json --noEmit", + "typecheck:all": "npm run typecheck && npm run typecheck:extension", + "export": "expo export", + "export:ios": "expo export --platform ios", + "export:android": "expo export --platform android", + "export:optimized": "EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1 EXPO_UNSTABLE_TREE_SHAKING=1 expo export", + "export:optimized:ios": "EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1 EXPO_UNSTABLE_TREE_SHAKING=1 expo export --platform ios", + "export:optimized:android": "EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1 EXPO_UNSTABLE_TREE_SHAKING=1 expo export --platform android", "ext": "wxt", "ext:build": "wxt build", "ext:build-prod": "BUILD_FOR_STORE=true wxt build", diff --git a/companion/services/calcom.ts b/companion/services/calcom.ts index 5e3c7f0964..18e4d349b8 100644 --- a/companion/services/calcom.ts +++ b/companion/services/calcom.ts @@ -803,8 +803,8 @@ export class CalComAPIService { } else { const keys = Object.keys(response.data); if (keys.length > 0) { - eventTypesArray = Object.values(response.data).filter( - (item): item is EventType => item && typeof item === "object" && "id" in item + eventTypesArray = Object.values(response.data).filter((item): item is EventType => + Boolean(item && typeof item === "object" && "id" in item) ) as EventType[]; } } @@ -900,9 +900,8 @@ export class CalComAPIService { // Convert object values to array as last resort const keys = Object.keys(response.data); if (keys.length > 0) { - bookingsArray = Object.values(response.data).filter( - (item): item is Booking => - item && typeof item === "object" && ("id" in item || "uid" in item) + bookingsArray = Object.values(response.data).filter((item): item is Booking => + Boolean(item && typeof item === "object" && ("id" in item || "uid" in item)) ) as Booking[]; } } diff --git a/companion/services/oauthService.ts b/companion/services/oauthService.ts index 38c6b314ba..df19ff8bc4 100644 --- a/companion/services/oauthService.ts +++ b/companion/services/oauthService.ts @@ -59,9 +59,10 @@ async function getExtensionSessionToken(): Promise { clearTimeout(timeoutId); window.removeEventListener("message", messageHandler); - extensionSessionToken = event.data.sessionToken || ""; + const token = event.data.sessionToken || ""; + extensionSessionToken = token; sessionTokenPromise = null; - resolve(extensionSessionToken); + resolve(token); }; window.addEventListener("message", messageHandler); @@ -253,10 +254,8 @@ export class CalComOAuthService { } // Try Firefox/Safari browser.identity API (Promise-based) - // @ts-ignore - Firefox/Safari use browser namespace - if (typeof browser !== "undefined" && browser.identity) { + if (typeof browser !== "undefined" && browser?.identity) { try { - // @ts-ignore - Firefox/Safari browser.identity returns Promise browser.identity .launchWebAuthFlow({ url: authUrl, interactive: true }) .then((responseUrl: string | undefined) => { @@ -553,7 +552,6 @@ function detectBrowserType(): BrowserType { const userAgent = navigator.userAgent.toLowerCase(); // Check for Brave first (it identifies as Chrome but has Brave-specific properties) - // @ts-ignore - Brave adds this to navigator if (navigator.brave && typeof navigator.brave.isBrave === "function") { return "brave"; } diff --git a/companion/services/types/bookings.types.ts b/companion/services/types/bookings.types.ts index 994b540a2b..bdbf6c95f4 100644 --- a/companion/services/types/bookings.types.ts +++ b/companion/services/types/bookings.types.ts @@ -48,7 +48,7 @@ export interface Booking { location?: string; cancellationReason?: string; rejectionReason?: string; - responses?: Record; + responses?: Record; } export interface GetBookingsResponse { diff --git a/companion/services/types/event-types.types.ts b/companion/services/types/event-types.types.ts index 99ea5c9e26..c0e38eef0d 100644 --- a/companion/services/types/event-types.types.ts +++ b/companion/services/types/event-types.types.ts @@ -285,7 +285,7 @@ export interface CreateEventTypeInput { lockTimeZoneToggleOnBookingPage?: boolean; successRedirectUrl?: string; forwardParamsSuccessRedirect?: boolean; - metadata?: Record; + metadata?: Record; } export interface GetEventTypesResponse { diff --git a/companion/services/types/users.types.ts b/companion/services/types/users.types.ts index aa1b92896f..7d3b40f706 100644 --- a/companion/services/types/users.types.ts +++ b/companion/services/types/users.types.ts @@ -15,7 +15,7 @@ export interface UserProfile { id: number; isPlatform: boolean; }; - metadata?: Record; + metadata?: Record; brandColor?: string; darkBrandColor?: string; theme?: string; diff --git a/companion/services/webAuth.ts b/companion/services/webAuth.ts index 0f05dd13b4..a99f7bc58b 100644 --- a/companion/services/webAuth.ts +++ b/companion/services/webAuth.ts @@ -1,10 +1,12 @@ import { Platform } from "react-native"; +import type { UserProfile } from "./types/users.types"; + export interface WebSessionInfo { isLoggedIn: boolean; accessToken?: string; refreshToken?: string; - userInfo?: any; + userInfo?: UserProfile; } export class WebAuthService { diff --git a/companion/tsconfig.extension.json b/companion/tsconfig.extension.json new file mode 100644 index 0000000000..9041e5ac6d --- /dev/null +++ b/companion/tsconfig.extension.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["chrome"] + }, + "include": ["extension/**/*.ts", "extension/**/*.tsx", "services/types/**/*.ts", "types/**/*.ts"], + "exclude": ["node_modules", ".output"] +} diff --git a/companion/tsconfig.json b/companion/tsconfig.json index 287f7c7f37..f3574c4672 100644 --- a/companion/tsconfig.json +++ b/companion/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "types": ["nativewind/types"] + "types": ["nativewind/types"], + "strict": true }, "extends": "expo/tsconfig.base", "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts", "nativewind-env.d.ts"], diff --git a/companion/types/attendee.ts b/companion/types/attendee.ts new file mode 100644 index 0000000000..04949e8834 --- /dev/null +++ b/companion/types/attendee.ts @@ -0,0 +1,7 @@ +import type { Booking } from "../services/types/bookings.types"; + +/** + * Attendee type extracted from Booking for reuse across the codebase. + * This provides a proper type for individual attendees instead of using `any`. + */ +export type Attendee = NonNullable[number]; diff --git a/companion/types/browser.d.ts b/companion/types/browser.d.ts new file mode 100644 index 0000000000..904e03f379 --- /dev/null +++ b/companion/types/browser.d.ts @@ -0,0 +1,29 @@ +/** + * Ambient declarations for browser extension APIs + * These provide proper typing for Firefox/Safari browser namespace + * and Brave-specific navigator properties used in oauthService.ts + */ + +// Firefox/Safari use 'browser' namespace instead of 'chrome' +// This is a WebExtension API that mirrors chrome.* APIs +// Firefox/Safari browser.identity API is Promise-based (unlike Chrome's callback-based API) +declare const browser: + | { + identity?: { + launchWebAuthFlow: (options: { + url: string; + interactive: boolean; + }) => Promise; + }; + runtime?: { + lastError?: { message?: string }; + }; + } + | undefined; + +// Brave adds isBrave() to navigator +interface Navigator { + brave?: { + isBrave: () => Promise; + }; +} diff --git a/companion/utils/shadows.ts b/companion/utils/shadows.ts index 761eb0835c..c116b8a0a6 100644 --- a/companion/utils/shadows.ts +++ b/companion/utils/shadows.ts @@ -24,9 +24,8 @@ export function createShadow(config: ShadowConfig = {}): ViewStyle { .toString(16) .padStart(2, "0"); return { - // @ts-ignore - web-only property boxShadow: `${offsetX}px ${offsetY}px ${radius}px rgba(0, 0, 0, ${opacity})`, - }; + } as ViewStyle; } return { diff --git a/companion/utils/slugify.ts b/companion/utils/slugify.ts index 26704f4077..01303b2896 100644 --- a/companion/utils/slugify.ts +++ b/companion/utils/slugify.ts @@ -10,11 +10,7 @@ export const slugify = (str: string, forDisplayingInput?: boolean) => { .toLowerCase() // Convert to lowercase .trim() // Remove whitespace from both sides .normalize("NFD") // Normalize to decomposed form for handling accents - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore .replace(/\p{Diacritic}/gu, "") // Remove any diacritics (accents) from characters - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore .replace(/[^.\p{L}\p{N}\p{Zs}\p{Emoji}]+/gu, "-") // Replace any non-alphanumeric characters (including Unicode and except "." period) with a dash .replace(/[\s_#]+/g, "-") // Replace whitespace, # and underscores with a single dash .replace(/^-+/, "") // Remove dashes from start