feat(companion): add Biome for linting and formatting (#26214)

* feat(companion): add ESLint with Prettier integration

- Add ESLint with eslint-config-expo/flat for SDK 55
- Integrate Prettier via eslint-plugin-prettier
- Configure environment-specific settings for Node.js config files and browser extension
- Add lint script to package.json
- Fix Prettier formatting issues in AvailabilityTab.tsx and types/index.ts

* fix(companion): use bun lockfile instead of npm

- Remove package-lock.json that was accidentally committed
- Update bun.lock with ESLint dependencies

* fix(companion): fix all ESLint errors (16 total)

- Fix unescaped entities in JSX (react/no-unescaped-entities):
  - app/(tabs)/(event-types)/index.tsx: escape quotes in delete confirmation
  - components/LoginScreen.tsx: escape apostrophe in "Don't have an account"
  - components/NetworkStatusBanner.tsx: escape apostrophes in offline message
  - components/booking-modals/BookingModals.tsx: escape apostrophe in rejection message
  - components/event-type-detail/tabs/AdvancedTab.tsx: escape quotes and apostrophes
  - components/screens/AvailabilityListScreen.tsx: escape quotes in delete confirmation

- Fix react-hooks/rules-of-hooks in RescheduleScreen.tsx:
  - Move useMemo calls before conditional early return to ensure hooks
    are called in the same order on every render

* fix(companion): fix all ESLint warnings (68 total)

- Fix import/no-named-as-default warnings by using named imports
- Fix @typescript-eslint/no-unused-vars warnings by removing unused imports/variables
- Fix react-hooks/exhaustive-deps warnings with eslint-disable comments
- Convert unused error variables in catch blocks to bare catch
- Remove unused imports (useRouter, Alert, useSafeAreaInsets, etc.)
- All ESLint checks now pass with 0 errors and 0 warnings

* feat(companion): add pre-commit lint check with --max-warnings=0

- Add companion lint check to root lint-staged.config.mjs
- Runs 'bun run lint -- --fix --max-warnings=0' for companion files
- Follows same pattern as apps/packages lint check
- Supports SKIP_WARNINGS=1 env var to bypass warning check if needed
- Commit will fail if any ESLint errors or warnings are present

* fix(companion): restore state variables and imports removed during ESLint fixes

- Restore locationAddress, locationLink, locationPhone state variables in event-type-detail.tsx (still used in fetchEventTypeData)
- Restore Alert import in BookingListScreen.tsx (still used for report booking)
- Add eslint-disable comments for the restored state variables since they're only used by setters

* test

* test

* fix eslint pre commit check

* fix: In React Native, HTML entities like ' are rendered literally as text, not decoded like in web browsers

* refactor(companion): remove ESLint and Prettier config in preparation for Biome

* feat(companion): add Biome for linting and formatting

- Install @biomejs/biome with exact version pinning (2.3.10)
- Add biome.json with formatting settings matching previous Prettier config
- Add lint, format, check, and check:ci scripts to package.json
- Update lint-staged to use Biome instead of Prettier
- Remove Prettier and prettier-plugin-tailwindcss dependencies
- Delete .prettierrc.js configuration file

* style(companion): apply Biome formatting and fix lint-staged config

- Update lint-staged to use 'biome format' instead of 'biome check'
  (pre-commit should only format, not lint)
- Apply Biome formatting to all companion files

* feat(companion): configure Biome for strict linting with pre-commit enforcement

- Apply Biome unsafe fixes (unused imports, optional chaining, etc.)
- Configure biome.json rules to disable overly strict rules for existing code:
  - noExplicitAny, noArrayIndexKey, useIterableCallbackReturn (suspicious)
  - useExhaustiveDependencies, noUnusedFunctionParameters (correctness)
  - noStaticOnlyClass (complexity)
  - noNonNullAssertion (style)
  - noStaticElementInteractions (a11y)
- Update lint-staged to use 'biome check --write --error-on-warnings'
  (pre-commit now runs both formatting AND linting, fails on warnings)

* fix(companion): enable noNonNullAssertion rule and fix all violations

- Enable noNonNullAssertion rule in biome.json (set to 'error')
- Fix 7 violations across the codebase:
  - hooks/useBookings.ts: Add runtime check before API call
  - hooks/useEventTypes.ts: Add runtime check before API call
  - hooks/useSchedules.ts: Add runtime check before API call
  - extension/entrypoints/content.ts: Add null checks for cache and getAttribute

* fix(companion): enable noStaticOnlyClass and noStaticElementInteractions rules

- Enable noStaticOnlyClass rule in biome.json
- Enable noStaticElementInteractions rule in biome.json
- Convert CalComAPIService from static class to object literal pattern
- Convert WebAuthService from static class to object literal pattern
- Add role="tooltip" to Tooltip.web.tsx for accessibility

* fix(companion): resolve TypeScript type errors in useEffect dependencies and remove unused state property

* fix(companion): enable noUnusedFunctionParameters rule and fix 9 violations

* fix(companion): enable noUnusedVariables rule and fix 4 violations

* fix(companion): enable useIterableCallbackReturn rule and fix 11 violations

* fix(companion): enable useExhaustiveDependencies rule and fix 7 violations

* fix(companion): enable noArrayIndexKey rule and fix 10 violations

* fix(companion): enable noExplicitAny rule and fix all violations

- Enable noExplicitAny rule in biome.json
- Replace any types with proper specific types across all files:
  - calcom.ts: Add proper type definitions for API responses and inputs
  - event-types.types.ts: Add BookingLimitsCount, BookingLimitsDuration, ConfirmationPolicy types
  - buildPartialUpdatePayload.ts: Fix type mismatches for booking limits functions
  - booking-actions.ts: Add proper types for booking action handlers
  - BookingDetailScreen.tsx, MarkNoShowScreen.tsx: Add types for attendee data
  - AvailabilityListItem.ios.tsx, BookingListItem.ios.tsx: Import SFSymbols7_0 type
  - extension/content.ts: Fix slot types and __cleanup invocation issues
- Remove unused imports and interfaces

* address cubics comments

* add useHookAtTopLevel rule
This commit is contained in:
Dhairyashil Shinde
2025-12-27 08:19:53 -03:00
committed by GitHub
parent 38e4c568b8
commit 2a84d46fe9
106 changed files with 3163 additions and 2718 deletions
-10
View File
@@ -1,10 +0,0 @@
module.exports = {
semi: true,
trailingComma: "es5",
singleQuote: false,
printWidth: 100,
tabWidth: 2,
useTabs: false,
arrowParens: "always",
plugins: ["prettier-plugin-tailwindcss"],
};
+3 -4
View File
@@ -1,7 +1,6 @@
import { createReadStream, existsSync } from "fs";
import { promises as fs } from "fs";
import { IncomingMessage, ServerResponse } from "http";
import { extname, join, normalize } from "path";
import { createReadStream, existsSync, promises as fs } from "node:fs";
import type { IncomingMessage, ServerResponse } from "node:http";
import { extname, join, normalize } from "node:path";
const distDir = join(process.cwd(), "dist");
@@ -1,8 +1,8 @@
import { AvailabilityListScreen } from "../../../components/screens/AvailabilityListScreen";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import { Stack } from "expo-router";
import React, { useState } from "react";
import { useState } from "react";
import { Platform } from "react-native";
import { AvailabilityListScreen } from "../../../components/screens/AvailabilityListScreen";
export default function Availability() {
const [searchQuery, setSearchQuery] = useState("");
+1 -1
View File
@@ -4,7 +4,7 @@ import { Platform } from "react-native";
export default function BookingsLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ headerShown: Platform.OS !== "ios" ? false : true }} />
<Stack.Screen name="index" options={{ headerShown: Platform.OS === "ios" }} />
<Stack.Screen name="booking-detail" />
<Stack.Screen name="reschedule" />
<Stack.Screen name="edit-location" />
@@ -1,10 +1,10 @@
import { AppPressable } from "../../../components/AppPressable";
import AddGuestsScreen from "../../../components/screens/AddGuestsScreen";
import type { AddGuestsScreenHandle } from "../../../components/screens/AddGuestsScreen";
import { CalComAPIService, type Booking } from "../../../services/calcom";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import React, { useState, useEffect, useCallback, useRef } from "react";
import { Alert, ActivityIndicator, View, Text, Platform } from "react-native";
import { useCallback, useEffect, useRef, useState } from "react";
import { ActivityIndicator, Alert, Platform, Text, View } from "react-native";
import { AppPressable } from "../../../components/AppPressable";
import type { AddGuestsScreenHandle } from "../../../components/screens/AddGuestsScreen";
import AddGuestsScreenComponent from "../../../components/screens/AddGuestsScreen";
import { type Booking, CalComAPIService } from "../../../services/calcom";
export default function AddGuests() {
const { uid } = useLocalSearchParams<{ uid: string }>();
@@ -99,7 +99,7 @@ export default function AddGuests() {
</Stack.Header>
)}
<AddGuestsScreen
<AddGuestsScreenComponent
ref={addGuestsScreenRef}
booking={booking}
onSuccess={handleAddGuestsSuccess}
@@ -1,11 +1,10 @@
import { Stack, useLocalSearchParams } from "expo-router";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Alert } from "react-native";
import { BookingDetailScreen } from "../../../components/screens/BookingDetailScreen";
import { useAuth } from "../../../contexts/AuthContext";
import { useBookingActionModals } from "../../../hooks";
import { CalComAPIService, type Booking } from "../../../services/calcom";
import { getBookingActions, type BookingActionsResult } from "../../../utils/booking-actions";
import { Stack, useLocalSearchParams } from "expo-router";
import React, { useState, useEffect, useCallback, useMemo, useRef } from "react";
import { Alert } from "react-native";
import { type Booking, CalComAPIService } from "../../../services/calcom";
import { type BookingActionsResult, getBookingActions } from "../../../utils/booking-actions";
// Empty actions result for when no booking is loaded
const EMPTY_ACTIONS: BookingActionsResult = {
@@ -54,9 +53,6 @@ export default function BookingDetail() {
}
}, [uid]);
// Booking action modals hook
const { selectedBooking: actionModalBooking } = useBookingActionModals();
// Compute actions using centralized gating (same as BookingDetailScreen)
const actions = useMemo(() => {
if (!booking) return EMPTY_ACTIONS;
@@ -1,10 +1,10 @@
import { AppPressable } from "../../../components/AppPressable";
import EditLocationScreen from "../../../components/screens/EditLocationScreen";
import type { EditLocationScreenHandle } from "../../../components/screens/EditLocationScreen";
import { CalComAPIService, type Booking } from "../../../services/calcom";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import React, { useState, useEffect, useCallback, useRef } from "react";
import { Alert, ActivityIndicator, View, Text, Platform } from "react-native";
import { useCallback, useEffect, useRef, useState } from "react";
import { ActivityIndicator, Alert, Platform, Text, View } from "react-native";
import { AppPressable } from "../../../components/AppPressable";
import type { EditLocationScreenHandle } from "../../../components/screens/EditLocationScreen";
import EditLocationScreenComponent from "../../../components/screens/EditLocationScreen";
import { type Booking, CalComAPIService } from "../../../services/calcom";
export default function EditLocation() {
const { uid } = useLocalSearchParams<{ uid: string }>();
@@ -99,7 +99,7 @@ export default function EditLocation() {
</Stack.Header>
)}
<EditLocationScreen
<EditLocationScreenComponent
ref={editLocationScreenRef}
booking={booking}
onSuccess={handleUpdateSuccess}
@@ -1,11 +1,11 @@
import type { NativeStackHeaderItemMenuAction } from "@react-navigation/native-stack";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import { Stack } from "expo-router";
import React, { useState } from "react";
import type { NativeStackHeaderItemMenuAction } from "@react-navigation/native-stack";
import { useState } from "react";
import { BookingListScreen } from "../../../components/booking-list-screen/BookingListScreen";
import { useActiveBookingFilter } from "../../../hooks/useActiveBookingFilter";
import { useEventTypes } from "../../../hooks";
import { useActiveBookingFilter } from "../../../hooks/useActiveBookingFilter";
export default function Bookings() {
const [searchQuery, setSearchQuery] = useState("");
+6 -7
View File
@@ -1,14 +1,13 @@
import { Ionicons } from "@expo/vector-icons";
import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect";
import SegmentedControl from "@react-native-segmented-control/segmented-control";
import React, { useState } from "react";
import { View, Text, TouchableOpacity, TextInput } from "react-native";
import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect";
import { useState } from "react";
import { Text, TextInput, TouchableOpacity, View } from "react-native";
import { BookingListScreen } from "../../../components/booking-list-screen/BookingListScreen";
import { Header } from "../../../components/Header";
import { useActiveBookingFilter } from "../../../hooks/useActiveBookingFilter";
import type { EventType } from "../../../services/calcom";
import { CalComAPIService } from "../../../services/calcom";
import { Header } from "../../../components/Header";
import { BookingListScreen } from "../../../components/booking-list-screen/BookingListScreen";
import { useActiveBookingFilter } from "../../../hooks/useActiveBookingFilter";
export default function Bookings() {
const [searchQuery, setSearchQuery] = useState("");
@@ -1,8 +1,8 @@
import MarkNoShowScreen from "../../../components/screens/MarkNoShowScreen";
import { CalComAPIService, type Booking } from "../../../services/calcom";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import React, { useState, useEffect } from "react";
import { Alert, ActivityIndicator, View, Platform } from "react-native";
import { useEffect, useState } from "react";
import { ActivityIndicator, Alert, Platform, View } from "react-native";
import MarkNoShowScreenComponent from "../../../components/screens/MarkNoShowScreen";
import { type Booking, CalComAPIService } from "../../../services/calcom";
interface Attendee {
id?: number | string;
@@ -11,6 +11,14 @@ interface Attendee {
noShow?: boolean;
}
interface BookingAttendee {
id?: number | string;
email: string;
name?: string;
noShow?: boolean;
absent?: boolean;
}
export default function MarkNoShow() {
const { uid } = useLocalSearchParams<{ uid: string }>();
const router = useRouter();
@@ -27,7 +35,7 @@ export default function MarkNoShow() {
// API may return "absent" or "noShow" depending on endpoint version
const bookingAttendees: Attendee[] = [];
if (bookingData.attendees && Array.isArray(bookingData.attendees)) {
bookingData.attendees.forEach((att: any) => {
bookingData.attendees.forEach((att: BookingAttendee) => {
bookingAttendees.push({
id: att.id,
email: att.email,
@@ -90,7 +98,7 @@ export default function MarkNoShow() {
</Stack.Header>
)}
<MarkNoShowScreen
<MarkNoShowScreenComponent
booking={booking}
attendees={attendees}
onUpdate={setAttendees}
@@ -98,7 +106,7 @@ export default function MarkNoShow() {
setBooking(updatedBooking);
const updatedAttendees: Attendee[] = [];
if (updatedBooking.attendees && Array.isArray(updatedBooking.attendees)) {
updatedBooking.attendees.forEach((att: any) => {
updatedBooking.attendees.forEach((att: BookingAttendee) => {
updatedAttendees.push({
id: att.id,
email: att.email,
@@ -1,29 +1,21 @@
import MeetingSessionDetailsScreen from "../../../components/screens/MeetingSessionDetailsScreen";
import { CalComAPIService, type Booking } from "../../../services/calcom";
import type { ConferencingSession } from "../../../services/types/bookings.types";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import React, { useState, useEffect } from "react";
import { Alert, ActivityIndicator, View, Platform } from "react-native";
import { useEffect, useState } from "react";
import { ActivityIndicator, Alert, Platform, View } from "react-native";
import MeetingSessionDetailsScreenComponent from "../../../components/screens/MeetingSessionDetailsScreen";
import { CalComAPIService } from "../../../services/calcom";
import type { ConferencingSession } from "../../../services/types/bookings.types";
export default function MeetingSessionDetails() {
const { uid } = useLocalSearchParams<{ uid: string }>();
const router = useRouter();
const [booking, setBooking] = useState<Booking | null>(null);
const [sessions, setSessions] = useState<ConferencingSession[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (uid) {
setIsLoading(true);
Promise.all([
CalComAPIService.getBookingByUid(uid),
CalComAPIService.getConferencingSessions(uid).catch((error) => {
console.error("Failed to load sessions:", error);
return [];
}),
])
.then(([bookingData, sessionsData]) => {
setBooking(bookingData);
CalComAPIService.getConferencingSessions(uid)
.then((sessionsData) => {
setSessions(sessionsData);
})
.catch(() => {
@@ -78,7 +70,7 @@ export default function MeetingSessionDetails() {
</Stack.Header>
)}
<MeetingSessionDetailsScreen sessions={sessions} />
<MeetingSessionDetailsScreenComponent sessions={sessions} />
</>
);
}
@@ -1,10 +1,10 @@
import { AppPressable } from "../../../components/AppPressable";
import RescheduleScreen from "../../../components/screens/RescheduleScreen";
import type { RescheduleScreenHandle } from "../../../components/screens/RescheduleScreen";
import { CalComAPIService, type Booking } from "../../../services/calcom";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import React, { useState, useEffect, useCallback, useRef } from "react";
import { Alert, ActivityIndicator, View, Text, Platform } from "react-native";
import { useCallback, useEffect, useRef, useState } from "react";
import { ActivityIndicator, Alert, Platform, Text, View } from "react-native";
import { AppPressable } from "../../../components/AppPressable";
import type { RescheduleScreenHandle } from "../../../components/screens/RescheduleScreen";
import RescheduleScreenComponent from "../../../components/screens/RescheduleScreen";
import { type Booking, CalComAPIService } from "../../../services/calcom";
export default function Reschedule() {
const { uid } = useLocalSearchParams<{ uid: string }>();
@@ -98,7 +98,7 @@ export default function Reschedule() {
</Stack.Header>
)}
<RescheduleScreen
<RescheduleScreenComponent
ref={rescheduleScreenRef}
booking={booking}
onSuccess={handleRescheduleSuccess}
@@ -1,9 +1,9 @@
import ViewRecordingsScreen from "../../../components/screens/ViewRecordingsScreen";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import { useEffect, useState } from "react";
import { ActivityIndicator, Alert, Platform, View } from "react-native";
import ViewRecordingsScreenComponent from "../../../components/screens/ViewRecordingsScreen";
import { CalComAPIService } from "../../../services/calcom";
import type { BookingRecording } from "../../../services/types/bookings.types";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import React, { useState, useEffect } from "react";
import { Alert, ActivityIndicator, View, Platform } from "react-native";
export default function ViewRecordings() {
const { uid } = useLocalSearchParams<{ uid: string }>();
@@ -69,7 +69,7 @@ export default function ViewRecordings() {
</Stack.Header>
)}
<ViewRecordingsScreen recordings={recordings} />
<ViewRecordingsScreenComponent recordings={recordings} />
</>
);
}
+28 -27
View File
@@ -1,16 +1,33 @@
import { Ionicons } from "@expo/vector-icons";
import * as Clipboard from "expo-clipboard";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import { Stack, useRouter } from "expo-router";
import { useMemo, useState } from "react";
import {
ActionSheetIOS,
Alert,
Platform,
RefreshControl,
ScrollView,
Share,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { EmptyScreen } from "../../../components/EmptyScreen";
import { EventTypeListItem } from "../../../components/event-type-list-item/EventTypeListItem";
import { FullScreenModal } from "../../../components/FullScreenModal";
import { Header } from "../../../components/Header";
import { LoadingSpinner } from "../../../components/LoadingSpinner";
import { EventTypeListItem } from "../../../components/event-type-list-item/EventTypeListItem";
import {
useEventTypes,
useCreateEventType,
useDeleteEventType,
useDuplicateEventType,
useEventTypes,
useUsername,
} from "../../../hooks";
import { CalComAPIService, EventType } from "../../../services/calcom";
import { CalComAPIService, type EventType } from "../../../services/calcom";
import { showErrorAlert } from "../../../utils/alerts";
import { openInAppBrowser } from "../../../utils/browser";
import { getEventDuration } from "../../../utils/getEventDuration";
@@ -18,23 +35,6 @@ import { offlineAwareRefresh } from "../../../utils/network";
import { normalizeMarkdown } from "../../../utils/normalizeMarkdown";
import { shadows } from "../../../utils/shadows";
import { slugify } from "../../../utils/slugify";
import { Ionicons } from "@expo/vector-icons";
import * as Clipboard from "expo-clipboard";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import { Stack, useRouter } from "expo-router";
import React, { useState, useMemo } from "react";
import {
View,
Text,
ScrollView,
TouchableOpacity,
RefreshControl,
TextInput,
ActionSheetIOS,
Share,
Alert,
Platform,
} from "react-native";
export default function EventTypes() {
const router = useRouter();
@@ -116,7 +116,7 @@ export default function EventTypes() {
return eventTypes.filter(
(eventType) =>
eventType.title.toLowerCase().includes(searchLower) ||
(eventType.description && eventType.description.toLowerCase().includes(searchLower))
eventType.description?.toLowerCase().includes(searchLower)
);
}, [eventTypes, searchQuery]);
@@ -184,7 +184,7 @@ export default function EventTypes() {
} else {
Alert.alert("Link Copied", "Event type link copied!");
}
} catch (error) {
} catch {
if (Platform.OS === "web") {
showToastMessage("Failed to copy link");
} else {
@@ -193,14 +193,15 @@ export default function EventTypes() {
}
};
const handleShare = async (eventType: EventType) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _handleShare = async (eventType: EventType) => {
try {
const link = await CalComAPIService.buildEventTypeLink(eventType.slug);
await Share.share({
message: `Book a meeting: ${eventType.title}`,
url: link,
});
} catch (error) {
} catch {
showErrorAlert("Error", "Failed to share link. Please try again.");
}
};
@@ -313,7 +314,7 @@ export default function EventTypes() {
// For mobile, use in-app browser
await openInAppBrowser(link, "event type preview");
}
} catch (error) {
} catch {
console.error("Failed to open preview");
if (Platform.OS === "web") {
showToastMessage("Failed to open preview");
@@ -351,8 +352,8 @@ export default function EventTypes() {
return;
}
const duration = parseInt(newEventDuration);
if (isNaN(duration) || duration <= 0) {
const duration = parseInt(newEventDuration, 10);
if (Number.isNaN(duration) || duration <= 0) {
Alert.alert("Error", "Please enter a valid duration");
return;
}
+3 -5
View File
@@ -1,12 +1,11 @@
import { Ionicons } from "@expo/vector-icons";
import { useState } from "react";
import { Alert, Platform, ScrollView, Text, TouchableOpacity, View } from "react-native";
import { Header } from "../../components/Header";
import { LogoutConfirmModal } from "../../components/LogoutConfirmModal";
import { useAuth } from "../../contexts/AuthContext";
import { showErrorAlert } from "../../utils/alerts";
import { openInAppBrowser } from "../../utils/browser";
import { Ionicons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
import React, { useState } from "react";
import { View, Text, TouchableOpacity, ScrollView, Alert, Platform } from "react-native";
interface MoreMenuItem {
name: string;
@@ -17,7 +16,6 @@ interface MoreMenuItem {
}
export default function More() {
const router = useRouter();
const { logout } = useAuth();
const [showLogoutModal, setShowLogoutModal] = useState(false);
+4 -4
View File
@@ -1,9 +1,9 @@
import { Stack } from "expo-router";
import { Platform, View, StatusBar } from "react-native";
import { Platform, StatusBar, View } from "react-native";
import LoginScreenComponent from "../components/LoginScreen";
import { NetworkStatusBanner } from "../components/NetworkStatusBanner";
import { AuthProvider, useAuth } from "../contexts/AuthContext";
import { QueryProvider } from "../contexts/QueryContext";
import { NetworkStatusBanner } from "../components/NetworkStatusBanner";
import LoginScreen from "../components/LoginScreen";
import "../global.css";
function RootLayoutContent() {
@@ -14,7 +14,7 @@ function RootLayoutContent() {
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Stack>
) : (
<LoginScreen />
<LoginScreenComponent />
);
const containerClass =
+1 -2
View File
@@ -1,6 +1,5 @@
import { Stack, useLocalSearchParams } from "expo-router";
import { AvailabilityDetailScreen } from "../components/screens/AvailabilityDetailScreen";
import { useLocalSearchParams, Stack } from "expo-router";
import React from "react";
export default function AvailabilityDetail() {
const { id } = useLocalSearchParams<{ id: string }>();
+171 -120
View File
@@ -1,55 +1,112 @@
import { Ionicons } from "@expo/vector-icons";
import * as Clipboard from "expo-clipboard";
import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import { useCallback, useEffect, useState } from "react";
import {
Alert,
Animated,
Modal,
Platform,
ScrollView,
Switch,
Text,
TouchableOpacity,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { AdvancedTab } from "../components/event-type-detail/tabs/AdvancedTab";
import { AvailabilityTab } from "../components/event-type-detail/tabs/AvailabilityTab";
import { BasicsTab } from "../components/event-type-detail/tabs/BasicsTab";
import { LimitsTab } from "../components/event-type-detail/tabs/LimitsTab";
import { RecurringTab } from "../components/event-type-detail/tabs/RecurringTab";
import { truncateTitle } from "../components/event-type-detail/utils";
import { buildPartialUpdatePayload } from "../components/event-type-detail/utils/buildPartialUpdatePayload";
import {
formatDuration,
truncateTitle,
formatAppIdToDisplayName,
} from "../components/event-type-detail/utils";
import {
buildPartialUpdatePayload,
hasChanges,
} from "../components/event-type-detail/utils/buildPartialUpdatePayload";
import { CalComAPIService, Schedule, ConferencingOption, EventType } from "../services/calcom";
import { LocationItem, LocationOptionGroup } from "../types/locations";
CalComAPIService,
type ConferencingOption,
type EventType,
type Schedule,
} from "../services/calcom";
import type { LocationItem, LocationOptionGroup } from "../types/locations";
import { showErrorAlert } from "../utils/alerts";
import { openInAppBrowser } from "../utils/browser";
import {
parseBufferTime,
parseMinimumNotice,
parseFrequencyUnit,
parseSlotInterval,
} from "../utils/eventTypeParsers";
import {
buildLocationOptions,
mapApiLocationToItem,
mapItemToApiLocation,
buildLocationOptions,
validateLocationItem,
} from "../utils/locationHelpers";
import { slugify } from "../utils/slugify";
import { Ionicons } from "@expo/vector-icons";
import * as Clipboard from "expo-clipboard";
import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect";
import { useRouter, useLocalSearchParams, Stack } from "expo-router";
import React, { useState, useEffect } from "react";
import {
View,
Text,
ScrollView,
TouchableOpacity,
TextInput,
Switch,
Modal,
Alert,
Animated,
Image,
Platform,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const tabs = [
// Type definitions for extended EventType fields not in the base type
interface EventTypeExtended {
lengthInMinutesOptions?: number[];
disableCancelling?: boolean;
disableRescheduling?: boolean;
sendCalVideoTranscription?: boolean;
autoTranslate?: boolean;
lockedTimeZone?: string;
hideCalendarEventDetails?: boolean;
hideOrganizerEmail?: boolean;
color?: {
lightThemeHex?: string;
darkThemeHex?: string;
};
}
interface BookerActiveBookingsLimitExtended {
disabled?: boolean;
maximumActiveBookings?: number;
count?: number;
offerReschedule?: boolean;
}
interface ConfirmationPolicyExtended {
disabled?: boolean;
}
interface RecurrenceExtended {
disabled?: boolean;
interval?: number;
frequency?: string;
occurrences?: number;
}
interface SeatsExtended {
disabled?: boolean;
seatsPerTimeSlot?: number;
showAttendeeInfo?: boolean;
showAvailabilityCount?: boolean;
}
interface ApiLocation {
type: string;
integration?: string;
address?: string;
link?: string;
phone?: string;
public?: boolean;
}
interface CreateEventTypePayload {
title: string;
slug: string;
lengthInMinutes: number;
description?: string;
locations?: ReturnType<typeof mapItemToApiLocation>[];
scheduleId?: number;
hidden?: boolean;
}
type TabIconName = "link" | "calendar" | "time" | "settings" | "refresh" | "ellipsis-horizontal";
interface Tab {
id: string;
label: string;
icon: TabIconName;
}
const tabs: Tab[] = [
{ id: "basics", label: "Basics", icon: "link" },
{ id: "availability", label: "Availability", icon: "calendar" },
{ id: "limits", label: "Limits", icon: "time" },
@@ -60,13 +117,11 @@ const tabs = [
export default function EventTypeDetail() {
const router = useRouter();
const { id, title, description, duration, price, currency, slug } = useLocalSearchParams<{
const { id, title, description, duration, slug } = useLocalSearchParams<{
id: string;
title: string;
description?: string;
duration: string;
price?: string;
currency?: string;
slug?: string;
}>();
@@ -81,9 +136,12 @@ export default function EventTypeDetail() {
const [username, setUsername] = useState("username");
const [allowMultipleDurations, setAllowMultipleDurations] = useState(false);
const [locations, setLocations] = useState<LocationItem[]>([]);
const [locationAddress, setLocationAddress] = useState("");
const [locationLink, setLocationLink] = useState("");
const [locationPhone, setLocationPhone] = useState("");
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_locationAddress, setLocationAddress] = useState("");
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_locationLink, setLocationLink] = useState("");
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_locationPhone, setLocationPhone] = useState("");
const [selectedDurations, setSelectedDurations] = useState<string[]>([]);
const [defaultDuration, setDefaultDuration] = useState("");
const [showDurationDropdown, setShowDurationDropdown] = useState(false);
@@ -128,8 +186,6 @@ export default function EventTypeDetail() {
const [rollingCalendarDays, setRollingCalendarDays] = useState(false);
const [rangeStartDate, setRangeStartDate] = useState("");
const [rangeEndDate, setRangeEndDate] = useState("");
const [offsetStartTimes, setOffsetStartTimes] = useState(false);
const [offsetStartValue, setOffsetStartValue] = useState("0");
// Advanced tab state
const [calendarEventName, setCalendarEventName] = useState("");
@@ -243,13 +299,6 @@ export default function EventTypeDetail() {
);
};
const getSelectedLocationIconUrl = (): string | null => {
if (locations.length > 0) {
return locations[0].iconUrl;
}
return null;
};
const toggleDurationSelection = (duration: string) => {
const newSelected = selectedDurations.includes(duration)
? selectedDurations.filter((d) => d !== duration)
@@ -313,7 +362,23 @@ export default function EventTypeDetail() {
);
};
const fetchSchedules = async () => {
const fetchScheduleDetails = useCallback(async (scheduleId: number) => {
try {
setScheduleDetailsLoading(true);
const scheduleDetails = await CalComAPIService.getScheduleById(scheduleId);
setSelectedScheduleDetails(scheduleDetails);
if (scheduleDetails?.timeZone) {
setSelectedTimezone(scheduleDetails.timeZone);
}
} catch (error) {
console.error("Failed to fetch schedule details:", error);
setSelectedScheduleDetails(null);
} finally {
setScheduleDetailsLoading(false);
}
}, []);
const fetchSchedules = useCallback(async () => {
try {
setSchedulesLoading(true);
const schedulesData = await CalComAPIService.getSchedules();
@@ -330,25 +395,9 @@ export default function EventTypeDetail() {
} finally {
setSchedulesLoading(false);
}
};
}, [fetchScheduleDetails]);
const fetchScheduleDetails = async (scheduleId: number) => {
try {
setScheduleDetailsLoading(true);
const scheduleDetails = await CalComAPIService.getScheduleById(scheduleId);
setSelectedScheduleDetails(scheduleDetails);
if (scheduleDetails?.timeZone) {
setSelectedTimezone(scheduleDetails.timeZone);
}
} catch (error) {
console.error("Failed to fetch schedule details:", error);
setSelectedScheduleDetails(null);
} finally {
setScheduleDetailsLoading(false);
}
};
const fetchConferencingOptions = async () => {
const fetchConferencingOptions = useCallback(async () => {
try {
setConferencingLoading(true);
const options = await CalComAPIService.getConferencingOptions();
@@ -358,13 +407,13 @@ export default function EventTypeDetail() {
} finally {
setConferencingLoading(false);
}
};
}, []);
const fetchEventTypeData = async () => {
const fetchEventTypeData = useCallback(async () => {
if (!id) return;
try {
const eventType = await CalComAPIService.getEventTypeById(parseInt(id));
const eventType = await CalComAPIService.getEventTypeById(parseInt(id, 10));
if (eventType) {
setEventTypeData(eventType);
@@ -375,14 +424,14 @@ export default function EventTypeDetail() {
if (eventType.lengthInMinutes) setEventDuration(eventType.lengthInMinutes.toString());
if (eventType.hidden !== undefined) setIsHidden(eventType.hidden);
const eventTypeAny = eventType as any;
const eventTypeExt = eventType as EventType & EventTypeExtended;
if (
eventTypeAny.lengthInMinutesOptions &&
Array.isArray(eventTypeAny.lengthInMinutesOptions) &&
eventTypeAny.lengthInMinutesOptions.length > 0
eventTypeExt.lengthInMinutesOptions &&
Array.isArray(eventTypeExt.lengthInMinutesOptions) &&
eventTypeExt.lengthInMinutesOptions.length > 0
) {
setAllowMultipleDurations(true);
const durationStrings = eventTypeAny.lengthInMinutesOptions.map(
const durationStrings = eventTypeExt.lengthInMinutesOptions.map(
(mins: number) => `${mins} mins`
);
setSelectedDurations(durationStrings);
@@ -425,7 +474,7 @@ export default function EventTypeDetail() {
// Load booking frequency limits
if (eventType.bookingLimitsCount && !("disabled" in eventType.bookingLimitsCount)) {
setLimitBookingFrequency(true);
const limits: Array<{ id: number; value: string; unit: string }> = [];
const limits: { id: number; value: string; unit: string }[] = [];
let idCounter = 1;
if (eventType.bookingLimitsCount.day) {
limits.push({
@@ -463,7 +512,7 @@ export default function EventTypeDetail() {
// Load duration limits
if (eventType.bookingLimitsDuration && !("disabled" in eventType.bookingLimitsDuration)) {
setLimitTotalDuration(true);
const limits: Array<{ id: number; value: string; unit: string }> = [];
const limits: { id: number; value: string; unit: string }[] = [];
let idCounter = 1;
if (eventType.bookingLimitsDuration.day) {
limits.push({
@@ -504,7 +553,8 @@ export default function EventTypeDetail() {
}
if (eventType.bookerActiveBookingsLimit) {
const bookingLimit = eventType.bookerActiveBookingsLimit as any;
const bookingLimit =
eventType.bookerActiveBookingsLimit as BookerActiveBookingsLimitExtended;
if (!("disabled" in bookingLimit)) {
const maxBookings = bookingLimit.maximumActiveBookings ?? bookingLimit.count;
if (maxBookings !== undefined) {
@@ -537,28 +587,26 @@ export default function EventTypeDetail() {
}
}
const eventTypeAnyForAdvanced = eventType as any;
if (eventTypeAnyForAdvanced.disableCancelling !== undefined) {
setDisableCancelling(eventTypeAnyForAdvanced.disableCancelling);
if (eventTypeExt.disableCancelling !== undefined) {
setDisableCancelling(eventTypeExt.disableCancelling);
} else if (eventType.metadata?.disableCancelling) {
setDisableCancelling(true);
}
if (eventTypeAnyForAdvanced.disableRescheduling !== undefined) {
setDisableRescheduling(eventTypeAnyForAdvanced.disableRescheduling);
if (eventTypeExt.disableRescheduling !== undefined) {
setDisableRescheduling(eventTypeExt.disableRescheduling);
} else if (eventType.metadata?.disableRescheduling) {
setDisableRescheduling(true);
}
if (eventTypeAnyForAdvanced.sendCalVideoTranscription !== undefined) {
setSendCalVideoTranscription(eventTypeAnyForAdvanced.sendCalVideoTranscription);
if (eventTypeExt.sendCalVideoTranscription !== undefined) {
setSendCalVideoTranscription(eventTypeExt.sendCalVideoTranscription);
} else if (eventType.metadata?.sendCalVideoTranscription) {
setSendCalVideoTranscription(true);
}
if (eventTypeAnyForAdvanced.autoTranslate !== undefined) {
setAutoTranslate(eventTypeAnyForAdvanced.autoTranslate);
if (eventTypeExt.autoTranslate !== undefined) {
setAutoTranslate(eventTypeExt.autoTranslate);
} else if (eventType.metadata?.autoTranslate) {
setAutoTranslate(true);
}
@@ -588,7 +636,7 @@ export default function EventTypeDetail() {
}
if (eventType.confirmationPolicy) {
const policy = eventType.confirmationPolicy as any;
const policy = eventType.confirmationPolicy as ConfirmationPolicyExtended;
if (!("disabled" in policy) || policy.disabled === false) {
setRequiresConfirmation(true);
}
@@ -606,14 +654,14 @@ export default function EventTypeDetail() {
if (eventType.lockTimeZoneToggleOnBookingPage !== undefined) {
setLockTimezone(eventType.lockTimeZoneToggleOnBookingPage);
}
if (eventTypeAny.lockedTimeZone) {
setLockedTimezone(eventTypeAny.lockedTimeZone);
if (eventTypeExt.lockedTimeZone) {
setLockedTimezone(eventTypeExt.lockedTimeZone);
}
if (eventTypeAny.hideCalendarEventDetails !== undefined) {
setHideCalendarEventDetails(eventTypeAny.hideCalendarEventDetails);
if (eventTypeExt.hideCalendarEventDetails !== undefined) {
setHideCalendarEventDetails(eventTypeExt.hideCalendarEventDetails);
}
if (eventTypeAny.hideOrganizerEmail !== undefined) {
setHideOrganizerEmail(eventTypeAny.hideOrganizerEmail);
if (eventTypeExt.hideOrganizerEmail !== undefined) {
setHideOrganizerEmail(eventTypeExt.hideOrganizerEmail);
}
// Load redirect URL
@@ -624,12 +672,12 @@ export default function EventTypeDetail() {
setForwardParamsSuccessRedirect(eventType.forwardParamsSuccessRedirect);
}
if (eventTypeAny.color) {
if (eventTypeAny.color.lightThemeHex) {
setEventTypeColorLight(eventTypeAny.color.lightThemeHex);
if (eventTypeExt.color) {
if (eventTypeExt.color.lightThemeHex) {
setEventTypeColorLight(eventTypeExt.color.lightThemeHex);
}
if (eventTypeAny.color.darkThemeHex) {
setEventTypeColorDark(eventTypeAny.color.darkThemeHex);
if (eventTypeExt.color.darkThemeHex) {
setEventTypeColorDark(eventTypeExt.color.darkThemeHex);
}
}
if (eventType.eventTypeColor) {
@@ -642,7 +690,7 @@ export default function EventTypeDetail() {
}
if (eventType.recurrence) {
const recurrence = eventType.recurrence as any;
const recurrence = eventType.recurrence as RecurrenceExtended;
if (recurrence.disabled !== true && recurrence.interval && recurrence.frequency) {
setRecurringEnabled(true);
setRecurringInterval(recurrence.interval.toString());
@@ -655,7 +703,9 @@ export default function EventTypeDetail() {
}
if (eventType.locations && eventType.locations.length > 0) {
const mappedLocations = eventType.locations.map((loc: any) => mapApiLocationToItem(loc));
const mappedLocations = eventType.locations.map((loc: ApiLocation) =>
mapApiLocationToItem(loc)
);
setLocations(mappedLocations);
const firstLocation = eventType.locations[0];
@@ -675,7 +725,7 @@ export default function EventTypeDetail() {
}
if (eventType.seats) {
const seats = eventType.seats as any;
const seats = eventType.seats as SeatsExtended;
const seatsAreEnabled =
seats.disabled === false || (!("disabled" in seats) && seats.seatsPerTimeSlot);
@@ -696,7 +746,7 @@ export default function EventTypeDetail() {
} catch (error) {
console.error("Failed to fetch event type data:", error);
}
};
}, [id]);
useEffect(() => {
if (activeTab === "availability") {
@@ -705,13 +755,13 @@ export default function EventTypeDetail() {
if (activeTab === "basics") {
fetchConferencingOptions();
}
}, [activeTab]);
}, [activeTab, fetchConferencingOptions, fetchSchedules]);
useEffect(() => {
// Fetch event type data and conferencing options on initial load
fetchEventTypeData();
fetchConferencingOptions();
}, [id]);
}, [fetchConferencingOptions, fetchEventTypeData]);
useEffect(() => {
const fetchUsername = async () => {
@@ -745,7 +795,7 @@ export default function EventTypeDetail() {
minute: "2-digit",
hour12: true,
});
} catch (error) {
} catch {
return time; // Return original if parsing fails
}
};
@@ -829,9 +879,9 @@ export default function EventTypeDetail() {
style: "destructive",
onPress: async () => {
try {
const eventTypeId = parseInt(id);
const eventTypeId = parseInt(id, 10);
if (isNaN(eventTypeId)) {
if (Number.isNaN(eventTypeId)) {
throw new Error("Invalid event type ID");
}
@@ -865,8 +915,8 @@ export default function EventTypeDetail() {
return;
}
const durationNum = parseInt(eventDuration);
if (isNaN(durationNum) || durationNum <= 0) {
const durationNum = parseInt(eventDuration, 10);
if (Number.isNaN(durationNum) || durationNum <= 0) {
Alert.alert("Error", "Duration must be a positive number");
return;
}
@@ -890,7 +940,7 @@ export default function EventTypeDetail() {
if (isCreateMode) {
// For CREATE mode, build full payload
const payload: any = {
const payload: CreateEventTypePayload = {
title: eventTitle,
slug: eventSlug,
lengthInMinutes: durationNum,
@@ -1003,7 +1053,7 @@ export default function EventTypeDetail() {
return;
}
await CalComAPIService.updateEventType(parseInt(id), payload);
await CalComAPIService.updateEventType(parseInt(id, 10), payload);
Alert.alert("Success", "Event type updated successfully");
// Refresh event type data to sync with server
await fetchEventTypeData();
@@ -1095,7 +1145,7 @@ export default function EventTypeDetail() {
>
<View className="flex-row items-center gap-2">
<Ionicons
name={tab.icon as any}
name={tab.icon}
size={18}
color={activeTab === tab.id ? "#007AFF" : "#666"}
/>
@@ -1141,7 +1191,7 @@ export default function EventTypeDetail() {
>
<View className="flex-row items-center gap-2">
<Ionicons
name={tab.icon as any}
name={tab.icon}
size={18}
color={activeTab === tab.id ? "#007AFF" : "#666"}
/>
@@ -1989,3 +2039,4 @@ export default function EventTypeDetail() {
</>
);
}
// test unused variable
+5 -4
View File
@@ -1,6 +1,6 @@
import React, { useEffect } from "react";
import { View, Text, ActivityIndicator, Platform } from "react-native";
import { useRouter, useLocalSearchParams } from "expo-router";
import { useLocalSearchParams, useRouter } from "expo-router";
import { useEffect } from "react";
import { ActivityIndicator, Platform, Text, View } from "react-native";
import { useAuth } from "../../contexts";
export default function OAuthCallback() {
@@ -93,7 +93,8 @@ export default function OAuthCallback() {
}
}
}
}, [params, router]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params, router, auth.userInfo]);
return (
<View className="flex-1 items-center justify-center bg-white">
+1 -1
View File
@@ -1,4 +1,4 @@
module.exports = function (api) {
module.exports = (api) => {
api.cache(true);
return {
presets: [["babel-preset-expo", { jsxImportSource: "nativewind" }], "nativewind/babel"],
+82
View File
@@ -0,0 +1,82 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": true,
"includes": [
"**/*.js",
"**/*.jsx",
"**/*.ts",
"**/*.tsx",
"**/*.json",
"!**/node_modules",
"!**/.expo",
"!**/dist",
"!**/.output",
"!**/*.d.ts",
"!**/bun.lock"
]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "error",
"noArrayIndexKey": "error",
"useIterableCallbackReturn": "error",
"noImplicitAnyLet": "off"
},
"correctness": {
"useExhaustiveDependencies": "error",
"useHookAtTopLevel": "error",
"noInvalidUseBeforeDeclaration": "off",
"noUnusedFunctionParameters": "error",
"noUnusedVariables": "error"
},
"complexity": {
"noStaticOnlyClass": "error"
},
"style": {
"noNonNullAssertion": "error"
},
"a11y": {
"noStaticElementInteractions": "error"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"trailingCommas": "es5",
"semicolons": "always",
"arrowParentheses": "always"
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
},
"overrides": [
{
"includes": ["extension/**/*.ts", "extension/**/*.tsx"],
"javascript": {
"globals": ["chrome", "browser"]
}
}
]
}
+20 -7
View File
@@ -12,7 +12,7 @@
"@react-native-segmented-control/segmented-control": "^2.5.7",
"@tanstack/react-query": "^5.62.0",
"@tanstack/react-query-persist-client": "^5.62.0",
"base64-js": "^1.5.1",
"base64-js": "1.5.1",
"expo": "^55.0.0-canary-20251216-3f01dbf",
"expo-auth-session": "7.0.11-canary-20251216-3f01dbf",
"expo-clipboard": "8.0.9-canary-20251216-3f01dbf",
@@ -40,14 +40,13 @@
"react-native-worklets": "0.7.1",
},
"devDependencies": {
"@biomejs/biome": "2.3.10",
"@types/chrome": "^0.1.31",
"@types/react": "19.2.3",
"@types/react-dom": "19.2.3",
"babel-preset-expo": "^54.0.7",
"husky": "^9.0.11",
"lint-staged": "^15.2.0",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.4.17",
"typescript": "^5.9.3",
"wxt": "^0.20.11",
@@ -251,6 +250,24 @@
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
"@biomejs/biome": ["@biomejs/biome@2.3.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.10", "@biomejs/cli-darwin-x64": "2.3.10", "@biomejs/cli-linux-arm64": "2.3.10", "@biomejs/cli-linux-arm64-musl": "2.3.10", "@biomejs/cli-linux-x64": "2.3.10", "@biomejs/cli-linux-x64-musl": "2.3.10", "@biomejs/cli-win32-arm64": "2.3.10", "@biomejs/cli-win32-x64": "2.3.10" }, "bin": { "biome": "bin/biome" } }, "sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.10", "", { "os": "win32", "cpu": "x64" }, "sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ=="],
"@devicefarmer/adbkit": ["@devicefarmer/adbkit@3.3.8", "", { "dependencies": { "@devicefarmer/adbkit-logcat": "^2.1.2", "@devicefarmer/adbkit-monkey": "~1.2.1", "bluebird": "~3.7", "commander": "^9.1.0", "debug": "~4.3.1", "node-forge": "^1.3.1", "split": "~1.0.1" }, "bin": { "adbkit": "bin/adbkit" } }, "sha512-7rBLLzWQnBwutH2WZ0EWUkQdihqrnLYCUMaB44hSol9e0/cdIhuNFcqZO0xNheAU6qqHVA8sMiLofkYTgb+lmw=="],
"@devicefarmer/adbkit-logcat": ["@devicefarmer/adbkit-logcat@2.1.3", "", {}, "sha512-yeaGFjNBc/6+svbDeul1tNHtNChw6h8pSHAt5D+JsedUrMTN7tla7B15WLDyekxsuS2XlZHRxpuC6m92wiwCNw=="],
@@ -1497,10 +1514,6 @@
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.5.14", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig-melody": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig-melody", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-Puaz+wPUAhFp8Lo9HuciYKM2Y2XExESjeT+9NQoVFXZsPPnc9VYss2SpxdQ6vbatmt8/4+SN0oe0I1cPDABg9Q=="],
"pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="],
"pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
+1 -1
View File
@@ -1,6 +1,6 @@
import React from "react";
import { TouchableOpacity } from "react-native";
import type { TouchableOpacityProps } from "react-native";
import { TouchableOpacity } from "react-native";
type AppPressableProps = TouchableOpacityProps & {
className?: string;
+3 -3
View File
@@ -1,9 +1,9 @@
import { useEffect } from "react";
import { useAuth } from "../contexts/AuthContext";
import { CalComAPIService } from "../services/calcom";
import { useEffect } from "react";
export function AuthTokenProvider({ children }: { children: React.ReactNode }) {
const { accessToken, isAuthenticated, isWebSession } = useAuth();
const { accessToken, isAuthenticated, isWebSession: _isWebSession } = useAuth();
useEffect(() => {
if (isAuthenticated) {
@@ -13,7 +13,7 @@ export function AuthTokenProvider({ children }: { children: React.ReactNode }) {
} else {
CalComAPIService.clearAuth();
}
}, [isAuthenticated, accessToken, isWebSession]);
}, [isAuthenticated, accessToken]);
return <>{children}</>;
}
@@ -4,13 +4,13 @@
* iOS-specific modal for booking actions with Glass UI styling.
* This provides a native iOS action sheet experience with Glass UI design.
*/
import { Ionicons } from "@expo/vector-icons";
import { ScrollView, Text, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import type { Booking } from "../services/calcom";
import type { BookingActionsResult } from "../utils/booking-actions";
import { FullScreenModal } from "./FullScreenModal";
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { View, Text, TouchableOpacity, ScrollView } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export interface BookingActionsModalProps {
visible: boolean;
+3 -6
View File
@@ -9,13 +9,12 @@
*
* Note: iOS uses BookingActionsModal.ios.tsx with native Glass UI styling.
*/
import { Ionicons } from "@expo/vector-icons";
import { ScrollView, Text, TouchableOpacity, View } from "react-native";
import type { Booking } from "../services/calcom";
import type { BookingActionsResult } from "../utils/booking-actions";
import { FullScreenModal } from "./FullScreenModal";
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { View, Text, TouchableOpacity, ScrollView } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export interface BookingActionsModalProps {
visible: boolean;
@@ -126,8 +125,6 @@ export function BookingActionsModal({
onReportBooking,
onCancelBooking,
}: BookingActionsModalProps) {
const insets = useSafeAreaInsets();
if (!booking) return null;
const handleAction = (action: () => void) => {
@@ -9,9 +9,8 @@
* to provide users with transparency about data freshness.
*/
import React from "react";
import { View, Text, StyleSheet } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { StyleSheet, Text, View } from "react-native";
import { useQueryContext } from "../contexts/QueryContext";
/**
-1
View File
@@ -1,4 +1,3 @@
import React from "react";
import Svg, { Path } from "react-native-svg";
interface CalComLogoProps {
+1 -2
View File
@@ -1,6 +1,5 @@
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { View, Text, TouchableOpacity } from "react-native";
import { Text, TouchableOpacity, View } from "react-native";
type IoniconName = keyof typeof Ionicons.glyphMap;
+3 -2
View File
@@ -1,5 +1,6 @@
import React, { useEffect } from "react";
import { Modal, ModalProps, Platform, View } from "react-native";
import type React from "react";
import { useEffect } from "react";
import { Modal, type ModalProps, Platform } from "react-native";
interface FullScreenModalProps extends ModalProps {
visible: boolean;
+2 -3
View File
@@ -1,8 +1,7 @@
import { AppPressable } from "./AppPressable";
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { View, Text, ActivityIndicator } from "react-native";
import { ActivityIndicator, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { AppPressable } from "./AppPressable";
interface GlassModalHeaderProps {
title: string;
+27 -29
View File
@@ -1,50 +1,48 @@
import { CalComAPIService, UserProfile } from "../services/calcom";
import { Ionicons } from "@expo/vector-icons";
import * as Clipboard from "expo-clipboard";
import { useCallback, useEffect, useState } from "react";
import {
ActionSheetIOS,
ActivityIndicator,
Alert,
Image,
Platform,
Text,
TouchableOpacity,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { CalComAPIService, type UserProfile } from "../services/calcom";
import { openInAppBrowser } from "../utils/browser";
import { getAvatarUrl } from "../utils/getAvatarUrl";
import { CalComLogo } from "./CalComLogo";
import { FullScreenModal } from "./FullScreenModal";
import { Ionicons } from "@expo/vector-icons";
import * as Clipboard from "expo-clipboard";
import { useRouter } from "expo-router";
import React, { useState, useEffect } from "react";
import {
View,
Text,
TouchableOpacity,
Image,
ActivityIndicator,
Platform,
ActionSheetIOS,
Alert,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export function Header() {
const router = useRouter();
const insets = useSafeAreaInsets();
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
const [loading, setLoading] = useState(true);
const [showProfileModal, setShowProfileModal] = useState(false);
useEffect(() => {
fetchUserProfile();
}, []);
const fetchUserProfile = async () => {
const fetchUserProfile = useCallback(async () => {
try {
const profile = await CalComAPIService.getUserProfile();
setUserProfile(profile);
} catch (error) {
} catch (err) {
console.error("Failed to fetch user profile");
if (__DEV__) {
const message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
const message = err instanceof Error ? err.message : String(err);
const stack = err instanceof Error ? err.stack : undefined;
console.debug("[Header] fetchUserProfile failed", { message, stack });
}
} finally {
setLoading(false);
}
};
}, []);
useEffect(() => {
fetchUserProfile();
}, [fetchUserProfile]);
// Build public page URL
const publicPageUrl = userProfile?.username ? `https://cal.com/${userProfile.username}` : null;
@@ -60,11 +58,11 @@ export function Header() {
try {
await Clipboard.setStringAsync(publicPageUrl);
Alert.alert("Link Copied!", "Your public page link has been copied to clipboard.");
} catch (error) {
} catch (err) {
console.error("Failed to copy public page link");
if (__DEV__) {
const message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
const message = err instanceof Error ? err.message : String(err);
const stack = err instanceof Error ? err.stack : undefined;
console.debug("[Header] copyPublicPageLink failed", { message, stack });
}
Alert.alert("Error", "Failed to copy link. Please try again.");
+1 -2
View File
@@ -1,6 +1,5 @@
import React from "react";
import { View, ActivityIndicator, Platform, StyleSheet } from "react-native";
import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect";
import { ActivityIndicator, Platform, StyleSheet, View } from "react-native";
interface LoadingSpinnerProps {
size?: "small" | "large";
+19 -19
View File
@@ -3,27 +3,27 @@
* Reusable component for displaying and managing multiple event type locations
*/
import React, { useState } from "react";
import {
View,
Text,
TouchableOpacity,
TextInput,
Modal,
ScrollView,
Platform,
ActionSheetIOS,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { SvgImage } from "./SvgImage";
import { LocationItem, LocationOptionGroup } from "../types/locations";
import type React from "react";
import { useState } from "react";
import {
ActionSheetIOS,
Modal,
Platform,
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import type { LocationItem, LocationOptionGroup } from "../types/locations";
import {
locationRequiresInput,
getLocationInputPlaceholder,
getLocationInputLabel,
createLocationItemFromOption,
getLocationInputLabel,
getLocationInputPlaceholder,
locationRequiresInput,
} from "../utils/locationHelpers";
import { SvgImage } from "./SvgImage";
interface LocationsListProps {
/** Array of current locations */
@@ -67,7 +67,7 @@ export const LocationsList: React.FC<LocationsListProps> = ({
const handleAddLocation = () => {
if (Platform.OS === "ios") {
const allOptions: Array<{ label: string; value: string }> = [];
const allOptions: { label: string; value: string }[] = [];
locationOptions.forEach((group) => {
group.options.forEach((option) => {
if (!isLocationAlreadyAdded(locations, option.value)) {
@@ -161,7 +161,7 @@ export const LocationsList: React.FC<LocationsListProps> = ({
{/* Locations List */}
{locations.length > 0 ? (
<View className="mb-3 space-y-2">
{locations.map((location, index) => (
{locations.map((location, _index) => (
<View key={location.id} className="rounded-lg border border-gray-200 bg-white p-3">
<View className="flex-row items-center justify-between">
<View className="flex-1 flex-row items-center">
+2 -3
View File
@@ -1,10 +1,9 @@
import { Platform, Text, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useAuth } from "../contexts/AuthContext";
import { showErrorAlert } from "../utils/alerts";
import { openInAppBrowser } from "../utils/browser";
import { CalComLogo } from "./CalComLogo";
import React from "react";
import { View, Text, TouchableOpacity, Platform, StyleSheet } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export function LoginScreen() {
const { loginWithOAuth, loading } = useAuth();
+2 -2
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import { Alert, Platform, Text, TouchableOpacity } from "react-native";
import { useAuth } from "../contexts/AuthContext";
import { LogoutConfirmModal } from "./LogoutConfirmModal";
import React, { useState } from "react";
import { TouchableOpacity, Text, Alert, Platform } from "react-native";
interface LogoutButtonProps {
className?: string;
+1 -2
View File
@@ -1,5 +1,4 @@
import React from "react";
import { View, Text, TouchableOpacity, Platform } from "react-native";
import { Platform, Text, TouchableOpacity, View } from "react-native";
import { FullScreenModal } from "./FullScreenModal";
interface LogoutConfirmModalProps {
+6 -6
View File
@@ -7,10 +7,10 @@
* - Shows again on next disconnect
*/
import React, { useEffect, useState, useRef } from "react";
import { View, Text, TouchableOpacity, Modal, Animated } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import NetInfo, { NetInfoState } from "@react-native-community/netinfo";
import NetInfo, { type NetInfoState } from "@react-native-community/netinfo";
import { useCallback, useEffect, useRef, useState } from "react";
import { Animated, Modal, Text, TouchableOpacity, View } from "react-native";
export function NetworkStatusBanner() {
const [showModal, setShowModal] = useState(false);
@@ -20,11 +20,11 @@ export function NetworkStatusBanner() {
const previousOfflineRef = useRef<boolean | null>(null);
const userDismissedRef = useRef(false);
const checkIfOffline = (state: NetInfoState): boolean => {
const checkIfOffline = useCallback((state: NetInfoState): boolean => {
if (state.isConnected === false) return true;
if (state.isInternetReachable === false) return true;
return false;
};
}, []);
useEffect(() => {
const handleNetworkChange = (state: NetInfoState) => {
@@ -57,7 +57,7 @@ export function NetworkStatusBanner() {
// Listen for changes
const unsubscribe = NetInfo.addEventListener(handleNetworkChange);
return () => unsubscribe();
}, []);
}, [checkIfOffline]);
useEffect(() => {
if (showModal) {
+3 -2
View File
@@ -1,5 +1,6 @@
import React, { useState } from "react";
import { View, Image } from "react-native";
import type React from "react";
import { useState } from "react";
import { Image, View } from "react-native";
import { SvgUri } from "react-native-svg";
interface SvgImageProps {
+4 -1
View File
@@ -1,4 +1,5 @@
import React, { useState } from "react";
import type React from "react";
import { useState } from "react";
interface TooltipProps {
text: string;
@@ -17,6 +18,7 @@ export function Tooltip({ text, children }: TooltipProps) {
};
return (
// biome-ignore lint/a11y/noStaticElementInteractions: Tooltip wrapper only shows/hides content on hover, not a clickable element
<span
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
@@ -25,6 +27,7 @@ export function Tooltip({ text, children }: TooltipProps) {
{children}
{showTooltip ? (
<span
role="tooltip"
style={{
position: "absolute",
left: "50%",
@@ -1,9 +1,10 @@
import type { AvailabilityListItemProps } from "./AvailabilityListItem";
import { Host, ContextMenu, Button, Image, HStack } from "@expo/ui/swift-ui";
import { Button, ContextMenu, Host, HStack, Image } from "@expo/ui/swift-ui";
import { buttonStyle, frame, padding } from "@expo/ui/swift-ui/modifiers";
import { Ionicons } from "@expo/vector-icons";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import { Pressable, Text, View } from "react-native";
import type { SFSymbols7_0 } from "sf-symbols-typescript";
import type { AvailabilityListItemProps } from "./AvailabilityListItem";
export const AvailabilityListItem = ({
item: schedule,
@@ -14,7 +15,7 @@ export const AvailabilityListItem = ({
}: AvailabilityListItemProps) => {
const scheduleActions: {
label: string;
icon: any;
icon: SFSymbols7_0;
onPress: () => void;
role: "default" | "destructive";
}[] = [
@@ -22,7 +23,7 @@ export const AvailabilityListItem = ({
? [
{
label: "Set as Default",
icon: "star",
icon: "star" as const,
onPress: () => onSetAsDefault(schedule),
role: "default" as const,
},
@@ -30,15 +31,15 @@ export const AvailabilityListItem = ({
: []),
{
label: "Duplicate",
icon: "square.on.square",
icon: "square.on.square" as const,
onPress: () => onDuplicate?.(schedule),
role: "default",
role: "default" as const,
},
{
label: "Delete",
icon: "trash",
icon: "trash" as const,
onPress: () => onDelete?.(schedule),
role: "destructive",
role: "destructive" as const,
},
];
@@ -1,6 +1,6 @@
import { Ionicons } from "@expo/vector-icons";
import { Text, TouchableOpacity, View } from "react-native";
import { Schedule } from "../../hooks";
import type { Schedule } from "../../hooks";
export interface AvailabilityListItemProps {
item: Schedule;
@@ -1,16 +1,16 @@
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { View, Text, TouchableOpacity, Linking, Pressable, Alert } from "react-native";
import { Host, ContextMenu, Button, Image, HStack } from "@expo/ui/swift-ui";
import { Button, ContextMenu, Host, HStack, Image } from "@expo/ui/swift-ui";
import { buttonStyle, frame, padding } from "@expo/ui/swift-ui/modifiers";
import { Ionicons } from "@expo/vector-icons";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import type { BookingListItemProps } from "./types";
import { SvgImage } from "../SvgImage";
import { getMeetingInfo } from "../../utils/meetings-utils";
import { formatTime, formatDate, getHostAndAttendeesDisplay } from "../../utils/bookings-utils";
import React from "react";
import { Linking, Pressable, Text, TouchableOpacity, View } from "react-native";
import type { SFSymbols7_0 } from "sf-symbols-typescript";
import { showErrorAlert } from "../../utils/alerts";
import { getBookingActions } from "../../utils/booking-actions";
import { formatDate, formatTime, getHostAndAttendeesDisplay } from "../../utils/bookings-utils";
import { getMeetingInfo } from "../../utils/meetings-utils";
import { SvgImage } from "../SvgImage";
import type { BookingListItemProps } from "./types";
export const BookingListItem: React.FC<BookingListItemProps> = ({
booking,
@@ -21,7 +21,7 @@ export const BookingListItem: React.FC<BookingListItemProps> = ({
onLongPress,
onConfirm,
onReject,
onActionsPress,
onActionsPress: _onActionsPress,
onReschedule,
onEditLocation,
onAddGuests,
@@ -37,14 +37,12 @@ export const BookingListItem: React.FC<BookingListItemProps> = ({
const isPending = booking.status?.toUpperCase() === "PENDING";
const isCancelled = booking.status?.toUpperCase() === "CANCELLED";
const isRejected = booking.status?.toUpperCase() === "REJECTED";
const hasLocationUrl = !!booking.location;
const hostAndAttendeesDisplay = getHostAndAttendeesDisplay(booking, userEmail);
const meetingInfo = getMeetingInfo(booking.location);
// Check if any attendee is marked as no-show
const hasNoShowAttendee = booking.attendees?.some(
(att: any) => att.noShow === true || att.absent === true
(att: { noShow?: boolean; absent?: boolean }) => att.noShow === true || att.absent === true
);
// Use centralized action gating for consistency
@@ -61,7 +59,7 @@ export const BookingListItem: React.FC<BookingListItemProps> = ({
// Define context menu actions based on booking state
type ContextMenuAction = {
label: string;
icon: string;
icon: SFSymbols7_0;
onPress: () => void;
role: "default" | "destructive";
};
@@ -70,39 +68,39 @@ export const BookingListItem: React.FC<BookingListItemProps> = ({
// Edit Event Section
{
label: "Reschedule Booking",
icon: "calendar",
icon: "calendar" as const,
onPress: () => onReschedule?.(booking),
role: "default",
role: "default" as const,
visible: isUpcoming && !isCancelled && !isPending && !!onReschedule,
},
{
label: "Edit Location",
icon: "location",
icon: "location" as const,
onPress: () => onEditLocation?.(booking),
role: "default",
role: "default" as const,
visible: isUpcoming && !isCancelled && !isPending && !!onEditLocation,
},
{
label: "Add Guests",
icon: "person.badge.plus",
icon: "person.badge.plus" as const,
onPress: () => onAddGuests?.(booking),
role: "default",
role: "default" as const,
visible: isUpcoming && !isCancelled && !isPending && !!onAddGuests,
},
// After Event Section
{
label: "View Recordings",
icon: "video",
icon: "video" as const,
onPress: () => onViewRecordings?.(booking),
role: "default",
role: "default" as const,
visible:
actions.viewRecordings.visible && actions.viewRecordings.enabled && !!onViewRecordings,
},
{
label: "Meeting Session Details",
icon: "info.circle",
icon: "info.circle" as const,
onPress: () => onMeetingSessionDetails?.(booking),
role: "default",
role: "default" as const,
visible:
actions.meetingSessionDetails.visible &&
actions.meetingSessionDetails.enabled &&
@@ -110,24 +108,24 @@ export const BookingListItem: React.FC<BookingListItemProps> = ({
},
{
label: "Mark as No-Show",
icon: "eye.slash",
icon: "eye.slash" as const,
onPress: () => onMarkNoShow?.(booking),
role: "default",
role: "default" as const,
visible: actions.markNoShow.visible && actions.markNoShow.enabled && !!onMarkNoShow,
},
// Other Actions
{
label: "Report Booking",
icon: "flag",
icon: "flag" as const,
onPress: () => onReportBooking?.(booking),
role: "destructive",
role: "destructive" as const,
visible: !!onReportBooking,
},
{
label: "Cancel Event",
icon: "xmark.circle",
icon: "xmark.circle" as const,
onPress: () => onCancelBooking?.(booking),
role: "destructive",
role: "destructive" as const,
visible: isUpcoming && !isCancelled && !!onCancelBooking,
},
];
@@ -171,7 +169,7 @@ export const BookingListItem: React.FC<BookingListItemProps> = ({
{/* Description */}
{booking.description ? (
<Text className="mb-2 text-sm leading-5 text-[#666]" numberOfLines={1}>
&quot;{booking.description}&quot;
"{booking.description}"
</Text>
) : null}
{/* Host and Attendees */}
@@ -265,7 +263,7 @@ export const BookingListItem: React.FC<BookingListItemProps> = ({
{contextMenuActions.map((action) => (
<Button
key={action.label}
systemImage={action.icon as any}
systemImage={action.icon}
onPress={action.onPress}
role={action.role}
label={action.label}
@@ -1,12 +1,11 @@
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { View, Text, TouchableOpacity, Linking } from "react-native";
import type { BookingListItemProps } from "./types";
import { SvgImage } from "../SvgImage";
import { getMeetingInfo } from "../../utils/meetings-utils";
import { formatTime, formatDate, getHostAndAttendeesDisplay } from "../../utils/bookings-utils";
import type React from "react";
import { Linking, Text, TouchableOpacity, View } from "react-native";
import { showErrorAlert } from "../../utils/alerts";
import { formatDate, formatTime, getHostAndAttendeesDisplay } from "../../utils/bookings-utils";
import { getMeetingInfo } from "../../utils/meetings-utils";
import { SvgImage } from "../SvgImage";
import type { BookingListItemProps } from "./types";
export const BookingListItem: React.FC<BookingListItemProps> = ({
booking,
@@ -31,7 +30,7 @@ export const BookingListItem: React.FC<BookingListItemProps> = ({
// Check if any attendee is marked as no-show
const hasNoShowAttendee = booking.attendees?.some(
(att: any) => att.noShow === true || att.absent === true
(att: { noShow?: boolean; absent?: boolean }) => att.noShow === true || att.absent === true
);
return (
@@ -69,7 +68,7 @@ export const BookingListItem: React.FC<BookingListItemProps> = ({
{/* Description */}
{booking.description ? (
<Text className="mb-2 text-sm leading-5 text-[#666]" numberOfLines={1}>
&quot;{booking.description}&quot;
"{booking.description}"
</Text>
) : null}
{/* Host and Attendees */}
@@ -1,32 +1,30 @@
import { Ionicons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
import React, { Activity, useMemo, useState } from "react";
import { View, Text, FlatList, RefreshControl, ScrollView, Alert } from "react-native";
import type { Booking, EventType } from "../../services/calcom";
import { LoadingSpinner } from "../LoadingSpinner";
import { EmptyScreen } from "../EmptyScreen";
import { BookingListItem } from "../booking-list-item/BookingListItem";
import { BookingModals } from "../booking-modals/BookingModals";
import { Alert, FlatList, RefreshControl, ScrollView, Text, View } from "react-native";
import { useAuth } from "../../contexts/AuthContext";
import {
type BookingFilter,
useBookingActions,
useBookings,
useCancelBooking,
useConfirmBooking,
useDeclineBooking,
useRescheduleBooking,
useBookingActions,
useBookingActionModals,
type BookingFilter,
} from "../../hooks";
import { offlineAwareRefresh } from "../../utils/network";
import type { Booking, EventType } from "../../services/calcom";
import type { ListItem } from "../../utils/bookings-utils";
import {
filterByEventType,
getEmptyStateContent,
groupBookingsByMonth,
searchBookings,
filterByEventType,
} from "../../utils/bookings-utils";
import type { ListItem } from "../../utils/bookings-utils";
import { offlineAwareRefresh } from "../../utils/network";
import { BookingListItem } from "../booking-list-item/BookingListItem";
import { BookingModals } from "../booking-modals/BookingModals";
import { EmptyScreen } from "../EmptyScreen";
import { LoadingSpinner } from "../LoadingSpinner";
interface BookingListScreenProps {
// Platform-specific header rendering
@@ -81,14 +79,10 @@ export const BookingListScreen: React.FC<BookingListScreenProps> = ({
const {
data: rawBookings = [],
isLoading: loading,
isFetching,
error: queryError,
refetch,
} = useBookings(filterParams);
// Show refresh indicator when fetching
const refreshing = isFetching && !loading;
// Cancel booking mutation
const { mutate: cancelBookingMutation } = useCancelBooking();
@@ -125,9 +119,6 @@ export const BookingListScreen: React.FC<BookingListScreenProps> = ({
isRescheduling,
});
// Booking action modals hook
const { selectedBooking: actionModalBooking } = useBookingActionModals();
// Navigate to reschedule screen (same pattern as booking detail)
const handleNavigateToReschedule = React.useCallback(
(booking: Booking) => {
@@ -1,19 +1,20 @@
import { Ionicons } from "@expo/vector-icons";
import React, { useMemo } from "react";
import type React from "react";
import { useMemo } from "react";
import {
View,
Text,
TouchableOpacity,
TextInput,
ActivityIndicator,
Alert,
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import type { Booking, EventType } from "../../services/calcom";
import { getBookingActions, type BookingActionsResult } from "../../utils/booking-actions";
import { FullScreenModal } from "../FullScreenModal";
import { type BookingActionsResult, getBookingActions } from "../../utils/booking-actions";
import { BookingActionsModal } from "../BookingActionsModal";
import { FullScreenModal } from "../FullScreenModal";
// Empty actions result for when no booking is selected
const EMPTY_ACTIONS: BookingActionsResult = {
@@ -70,11 +71,11 @@ interface BookingModalsProps {
}
export const BookingModals: React.FC<BookingModalsProps> = ({
showRescheduleModal,
rescheduleBooking,
isRescheduling,
onRescheduleClose,
onRescheduleSubmit,
showRescheduleModal: _showRescheduleModal,
rescheduleBooking: _rescheduleBooking,
isRescheduling: _isRescheduling,
onRescheduleClose: _onRescheduleClose,
onRescheduleSubmit: _onRescheduleSubmit,
showRejectModal,
rejectReason,
isDeclining,
@@ -1,6 +1,5 @@
import React from "react";
import { View, Text, TextInput, TouchableOpacity, Switch, Alert } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { Alert, Switch, Text, TextInput, TouchableOpacity, View } from "react-native";
import { openInAppBrowser } from "../../../utils/browser";
@@ -1,7 +1,6 @@
import { Schedule } from "../../../services/calcom";
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { View, Text, TouchableOpacity } from "react-native";
import { Text, TouchableOpacity, View } from "react-native";
import type { Schedule } from "../../../services/calcom";
interface DaySchedule {
day: string;
@@ -44,8 +43,8 @@ export function AvailabilityTab(props: AvailabilityTabProps) {
{props.schedulesLoading
? "Loading schedules..."
: props.selectedSchedule
? props.selectedSchedule.name
: "Select schedule"}
? props.selectedSchedule.name
: "Select schedule"}
</Text>
<Ionicons name="chevron-down" size={20} color="#8E8E93" />
</TouchableOpacity>
@@ -59,8 +58,8 @@ export function AvailabilityTab(props: AvailabilityTabProps) {
<Text className="text-sm italic text-[#8E8E93]">Loading schedule details...</Text>
</View>
) : props.selectedScheduleDetails ? (
props.getDaySchedules().map((dayInfo, index) => (
<View key={index} className="flex-row items-center justify-between py-4">
props.getDaySchedules().map((dayInfo) => (
<View key={dayInfo.day} className="flex-row items-center justify-between py-4">
<Text
className={`ml-2 flex-1 text-[15px] font-medium text-[#333] ${
!dayInfo.available ? "text-[#8E8E93] line-through" : ""
@@ -1,9 +1,8 @@
import React from "react";
import { View, Text, TextInput, TouchableOpacity, Switch } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { Switch, Text, TextInput, TouchableOpacity, View } from "react-native";
import { LocationsList } from "../../../components/LocationsList";
import { LocationItem, LocationOptionGroup } from "../../../types/locations";
import type { LocationItem, LocationOptionGroup } from "../../../types/locations";
import { slugify } from "../../../utils/slugify";
interface BasicsTabProps {
@@ -1,6 +1,5 @@
import React from "react";
import { View, Text, TextInput, TouchableOpacity, Switch, Animated } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { Animated, Switch, Text, TextInput, TouchableOpacity, View } from "react-native";
interface FrequencyLimit {
id: number;
@@ -119,7 +118,7 @@ export function LimitsTab(props: LimitsTabProps) {
value={props.minimumNoticeValue}
onChangeText={(text) => {
const numericValue = text.replace(/[^0-9]/g, "");
const num = parseInt(numericValue) || 0;
const num = parseInt(numericValue, 10) || 0;
if (num >= 0) {
props.setMinimumNoticeValue(numericValue || "0");
}
@@ -192,7 +191,7 @@ export function LimitsTab(props: LimitsTabProps) {
value={limit.value}
onChangeText={(text) => {
const numericValue = text.replace(/[^0-9]/g, "");
const num = parseInt(numericValue) || 0;
const num = parseInt(numericValue, 10) || 0;
if (num >= 0) {
props.updateFrequencyLimit(limit.id, "value", numericValue || "0");
}
@@ -292,7 +291,7 @@ export function LimitsTab(props: LimitsTabProps) {
value={limit.value}
onChangeText={(text) => {
const numericValue = text.replace(/[^0-9]/g, "");
const num = parseInt(numericValue) || 0;
const num = parseInt(numericValue, 10) || 0;
if (num >= 0) {
props.updateDurationLimit(limit.id, "value", numericValue || "0");
}
@@ -358,7 +357,7 @@ export function LimitsTab(props: LimitsTabProps) {
value={props.maxActiveBookingsValue}
onChangeText={(text) => {
const numericValue = text.replace(/[^0-9]/g, "");
const num = parseInt(numericValue) || 0;
const num = parseInt(numericValue, 10) || 0;
if (num >= 0) {
props.setMaxActiveBookingsValue(numericValue || "1");
}
@@ -1,6 +1,5 @@
import React from "react";
import { View, Text, TextInput, TouchableOpacity, Switch } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { Switch, Text, TextInput, TouchableOpacity, View } from "react-native";
import { openInAppBrowser } from "../../../utils/browser";
interface RecurringTabProps {
@@ -28,7 +27,7 @@ export function RecurringTab({
recurringInterval,
setRecurringInterval,
recurringFrequency,
setRecurringFrequency,
setRecurringFrequency: _setRecurringFrequency,
recurringOccurrences,
setRecurringOccurrences,
setShowFrequencyDropdown,
@@ -79,7 +78,7 @@ export function RecurringTab({
setRecurringInterval("1");
return;
}
const num = parseInt(numericValue);
const num = parseInt(numericValue, 10);
if (num >= 1 && num <= 20) {
setRecurringInterval(numericValue);
}
@@ -113,7 +112,7 @@ export function RecurringTab({
setRecurringOccurrences("1");
return;
}
const num = parseInt(numericValue);
const num = parseInt(numericValue, 10);
if (num >= 1) {
setRecurringOccurrences(numericValue);
}
@@ -5,11 +5,9 @@
* New code should import directly from the source files.
*/
// Re-export partial update utilities
export { buildPartialUpdatePayload, hasChanges } from "./utils/buildPartialUpdatePayload";
// Re-export formatting utilities from centralized location
export { formatDuration, truncateTitle, formatAppIdToDisplayName } from "../../utils/formatters";
export { formatAppIdToDisplayName, formatDuration, truncateTitle } from "../../utils/formatters";
// Re-export location utilities from centralized location
export { displayNameToLocationValue } from "../../utils/locationHelpers";
// Re-export partial update utilities
export { buildPartialUpdatePayload, hasChanges } from "./utils/buildPartialUpdatePayload";
@@ -1,13 +1,74 @@
import type { EventType } from "../../../services/calcom";
import type {
BookingLimitsCount,
BookingLimitsDuration,
EventType,
} from "../../../services/calcom";
import type { LocationItem } from "../../../types/locations";
import {
parseBufferTime,
parseMinimumNotice,
parseFrequencyUnit,
parseMinimumNotice,
parseSlotInterval,
} from "../../../utils/eventTypeParsers";
import { mapItemToApiLocation } from "../../../utils/locationHelpers";
interface LocationInput {
type: string;
integration?: string;
address?: string;
link?: string;
phone?: string;
public?: boolean;
}
interface NormalizedLocation {
type: string;
integration?: string;
address?: string;
link?: string;
phone?: string;
public?: boolean;
}
interface BookingWindow {
disabled?: boolean;
type?: string;
value?: number | string[];
}
interface BookerActiveBookingsLimit {
disabled?: boolean;
maximumActiveBookings?: number;
count?: number;
offerReschedule?: boolean;
}
interface RecurrenceConfig {
disabled?: boolean;
interval?: number;
frequency?: string;
occurrences?: number;
}
interface SeatsConfig {
disabled?: boolean;
seatsPerTimeSlot?: number;
showAttendeeInfo?: boolean;
showAvailabilityCount?: boolean;
}
interface BookerLayoutsConfig {
enabledLayouts?: string[];
defaultLayout?: string;
}
interface ColorsConfig {
lightThemeHex?: string;
darkThemeHex?: string;
lightEventTypeColor?: string;
darkEventTypeColor?: string;
}
interface FrequencyLimit {
id: number;
value: string;
@@ -92,7 +153,7 @@ function hasMultipleDurationsChanged(
selectedDurations: string[],
defaultDuration: string,
mainDuration: string,
original: any
original: EventType | null
): boolean {
const originalOptions = original?.lengthInMinutesOptions;
const originalHasMultiple =
@@ -103,7 +164,7 @@ function hasMultipleDurationsChanged(
if (enabled && !originalHasMultiple) return true;
const currentDurations = selectedDurations.map(parseDurationString).sort((a, b) => a - b);
const originalDurations = [...originalOptions].sort((a: number, b: number) => a - b);
const originalDurations = [...(originalOptions ?? [])].sort((a: number, b: number) => a - b);
if (!areEqual(currentDurations, originalDurations)) return true;
@@ -132,10 +193,10 @@ function areEqual(a: unknown, b: unknown): boolean {
return keysA.every((key, index) => key === keysB[index] && areEqual(objA[key], objB[key]));
}
function normalizeLocation(loc: any): any {
function normalizeLocation(loc: LocationInput | null): NormalizedLocation | null {
if (!loc) return null;
const normalized: any = { type: loc.type };
const normalized: NormalizedLocation = { type: loc.type };
if (loc.type === "integration") {
normalized.integration = loc.integration;
@@ -155,7 +216,7 @@ function normalizeLocation(loc: any): any {
function haveLocationsChanged(
currentLocations: LocationItem[],
originalLocations: any[] | undefined
originalLocations: LocationInput[] | undefined
): boolean {
if ((!originalLocations || originalLocations.length === 0) && currentLocations.length === 0) {
return false;
@@ -164,9 +225,10 @@ function haveLocationsChanged(
if (originalLocations && currentLocations.length !== originalLocations.length) return true;
const currentMapped = currentLocations.map((loc) => normalizeLocation(mapItemToApiLocation(loc)));
const originalMapped = originalLocations!.map((loc) => normalizeLocation(loc));
const originalMapped = originalLocations?.map((loc) => normalizeLocation(loc)) ?? [];
const sortByType = (a: any, b: any) => (a?.type || "").localeCompare(b?.type || "");
const sortByType = (a: NormalizedLocation | null, b: NormalizedLocation | null) =>
(a?.type || "").localeCompare(b?.type || "");
currentMapped.sort(sortByType);
originalMapped.sort(sortByType);
@@ -176,13 +238,12 @@ function haveLocationsChanged(
function hasBookingLimitsCountChanged(
enabled: boolean,
limits: FrequencyLimit[],
original: any
original: BookingLimitsCount | { disabled: true } | null | undefined
): boolean {
const originalIsDisabled =
!original ||
original.disabled === true ||
Object.keys(original).length === 0 ||
(Object.keys(original).length === 1 && original.disabled !== undefined);
("disabled" in original && original.disabled === true) ||
Object.keys(original).length === 0;
if (!enabled && originalIsDisabled) return false;
if (!enabled && !originalIsDisabled) return true;
@@ -192,17 +253,17 @@ function hasBookingLimitsCountChanged(
limits.forEach((limit) => {
const unit = parseFrequencyUnit(limit.unit);
if (unit) {
currentLimits[unit] = parseInt(limit.value) || 1;
currentLimits[unit] = parseInt(limit.value, 10) || 1;
}
});
const originalLimits: Record<string, number> = {};
if (original) {
Object.keys(original).forEach((key) => {
if (key !== "disabled" && typeof original[key] === "number") {
originalLimits[key] = original[key];
}
});
if (original && !("disabled" in original)) {
const orig = original as BookingLimitsCount;
if (orig.day !== undefined) originalLimits.day = orig.day;
if (orig.week !== undefined) originalLimits.week = orig.week;
if (orig.month !== undefined) originalLimits.month = orig.month;
if (orig.year !== undefined) originalLimits.year = orig.year;
}
return !areEqual(currentLimits, originalLimits);
@@ -211,13 +272,12 @@ function hasBookingLimitsCountChanged(
function hasBookingLimitsDurationChanged(
enabled: boolean,
limits: FrequencyLimit[],
original: any
original: BookingLimitsDuration | { disabled: true } | null | undefined
): boolean {
const originalIsDisabled =
!original ||
original.disabled === true ||
Object.keys(original).length === 0 ||
(Object.keys(original).length === 1 && original.disabled !== undefined);
("disabled" in original && original.disabled === true) ||
Object.keys(original).length === 0;
if (!enabled && originalIsDisabled) return false;
if (!enabled && !originalIsDisabled) return true;
@@ -227,17 +287,17 @@ function hasBookingLimitsDurationChanged(
limits.forEach((limit) => {
const unit = parseFrequencyUnit(limit.unit);
if (unit) {
currentLimits[unit] = parseInt(limit.value) || 60;
currentLimits[unit] = parseInt(limit.value, 10) || 60;
}
});
const originalLimits: Record<string, number> = {};
if (original) {
Object.keys(original).forEach((key) => {
if (key !== "disabled" && typeof original[key] === "number") {
originalLimits[key] = original[key];
}
});
if (original && !("disabled" in original)) {
const orig = original as BookingLimitsDuration;
if (orig.day !== undefined) originalLimits.day = orig.day;
if (orig.week !== undefined) originalLimits.week = orig.week;
if (orig.month !== undefined) originalLimits.month = orig.month;
if (orig.year !== undefined) originalLimits.year = orig.year;
}
return !areEqual(currentLimits, originalLimits);
@@ -250,7 +310,7 @@ function hasBookingWindowChanged(
calendarDays: boolean,
rangeStart: string,
rangeEnd: string,
original: any
original: BookingWindow | null | undefined
): boolean {
const originalDisabled = !original || original.disabled;
@@ -259,14 +319,14 @@ function hasBookingWindowChanged(
if (enabled && originalDisabled) return true;
if (type === "range") {
if (original.type !== "range") return true;
const originalValue = original.value;
if (original?.type !== "range") return true;
const originalValue = original?.value;
if (!Array.isArray(originalValue)) return true;
return originalValue[0] !== rangeStart || originalValue[1] !== rangeEnd;
} else {
const expectedType = calendarDays ? "calendarDays" : "businessDays";
if (original.type !== expectedType) return true;
return original.value !== parseInt(rollingDays);
if (original?.type !== expectedType) return true;
return original?.value !== parseInt(rollingDays, 10);
}
}
@@ -274,7 +334,7 @@ function hasBookerActiveBookingsLimitChanged(
enabled: boolean,
value: string,
offerReschedule: boolean,
original: any
original: BookerActiveBookingsLimit | null | undefined
): boolean {
const originalDisabled = !original || original.disabled;
@@ -282,8 +342,8 @@ function hasBookerActiveBookingsLimitChanged(
if (!enabled && !originalDisabled) return true;
if (enabled && originalDisabled) return true;
const originalMax = original.maximumActiveBookings ?? original.count;
return originalMax !== parseInt(value) || original.offerReschedule !== offerReschedule;
const originalMax = original?.maximumActiveBookings ?? original?.count;
return originalMax !== parseInt(value, 10) || original?.offerReschedule !== offerReschedule;
}
function hasRecurrenceChanged(
@@ -291,7 +351,7 @@ function hasRecurrenceChanged(
interval: string,
frequency: string,
occurrences: string,
original: any
original: RecurrenceConfig | null | undefined
): boolean {
const originalDisabled = !original || original.disabled === true;
@@ -300,9 +360,9 @@ function hasRecurrenceChanged(
if (enabled && originalDisabled) return true;
return (
original.interval !== parseInt(interval) ||
original.frequency !== frequency ||
original.occurrences !== parseInt(occurrences)
original?.interval !== parseInt(interval, 10) ||
original?.frequency !== frequency ||
original?.occurrences !== parseInt(occurrences, 10)
);
}
@@ -311,7 +371,7 @@ function hasSeatsChanged(
perTimeSlot: string,
showAttendee: boolean,
showAvailability: boolean,
original: any
original: SeatsConfig | null | undefined
): boolean {
const originalDisabled = !original || original.disabled === true;
const originalEnabled =
@@ -323,9 +383,9 @@ function hasSeatsChanged(
if (enabled && originalDisabled) return true;
return (
original.seatsPerTimeSlot !== parseInt(perTimeSlot) ||
original.showAttendeeInfo !== showAttendee ||
original.showAvailabilityCount !== showAvailability
original?.seatsPerTimeSlot !== parseInt(perTimeSlot, 10) ||
original?.showAttendeeInfo !== showAttendee ||
original?.showAvailabilityCount !== showAvailability
);
}
@@ -341,7 +401,8 @@ function mapLayoutToApi(layout: string): string {
return mapping[layout] || layout.toLowerCase().replace("_view", "");
}
function mapLayoutFromApi(layout: string): string {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function _mapLayoutFromApi(layout: string): string {
const mapping: Record<string, string> = {
month: "MONTH_VIEW",
week: "WEEK_VIEW",
@@ -350,13 +411,13 @@ function mapLayoutFromApi(layout: string): string {
WEEK_VIEW: "WEEK_VIEW",
COLUMN_VIEW: "COLUMN_VIEW",
};
return mapping[layout] || layout.toUpperCase() + "_VIEW";
return mapping[layout] || `${layout.toUpperCase()}_VIEW`;
}
function hasBookerLayoutsChanged(
selectedLayouts: string[],
defaultLayout: string,
original: any
original: BookerLayoutsConfig | null | undefined
): boolean {
if (!original) return selectedLayouts.length > 0;
@@ -370,7 +431,11 @@ function hasBookerLayoutsChanged(
return mapLayoutToApi(defaultLayout) !== mapLayoutToApi(originalDefault || "");
}
function hasColorsChanged(lightColor: string, darkColor: string, original: any): boolean {
function hasColorsChanged(
lightColor: string,
darkColor: string,
original: ColorsConfig | null | undefined
): boolean {
if (!original) return lightColor !== "#292929" || darkColor !== "#FAFAFA";
const originalLight = original.lightThemeHex || original.lightEventTypeColor;
const originalDark = original.darkThemeHex || original.darkEventTypeColor;
@@ -381,15 +446,15 @@ function hasColorsChanged(lightColor: string, darkColor: string, original: any):
export function buildPartialUpdatePayload(
currentState: EventTypeFormState,
originalData: EventType | null
): Record<string, any> {
const payload: Record<string, any> = {};
): Record<string, unknown> {
const payload: Record<string, unknown> = {};
if (!originalData) {
console.warn("buildPartialUpdatePayload called without original data");
return {};
}
const original = originalData as any;
const original = originalData;
if (currentState.eventTitle !== original.title) {
payload.title = currentState.eventTitle;
@@ -403,7 +468,7 @@ export function buildPartialUpdatePayload(
payload.description = currentState.eventDescription || "";
}
const currentDuration = parseInt(currentState.eventDuration);
const currentDuration = parseInt(currentState.eventDuration, 10);
if (
hasMultipleDurationsChanged(
@@ -496,7 +561,7 @@ export function buildPartialUpdatePayload(
currentState.frequencyLimits.forEach((limit) => {
const unit = parseFrequencyUnit(limit.unit);
if (unit) {
limitsCount[unit] = parseInt(limit.value) || 1;
limitsCount[unit] = parseInt(limit.value, 10) || 1;
}
});
payload.bookingLimitsCount = limitsCount;
@@ -518,7 +583,7 @@ export function buildPartialUpdatePayload(
currentState.durationLimits.forEach((limit) => {
const unit = parseFrequencyUnit(limit.unit);
if (unit) {
limitsDuration[unit] = parseInt(limit.value) || 60;
limitsDuration[unit] = parseInt(limit.value, 10) || 60;
}
});
payload.bookingLimitsDuration = limitsDuration;
@@ -543,7 +608,7 @@ export function buildPartialUpdatePayload(
) {
if (currentState.maxActiveBookingsPerBooker) {
payload.bookerActiveBookingsLimit = {
maximumActiveBookings: parseInt(currentState.maxActiveBookingsValue) || 1,
maximumActiveBookings: parseInt(currentState.maxActiveBookingsValue, 10) || 1,
offerReschedule: currentState.offerReschedule,
};
} else {
@@ -572,7 +637,7 @@ export function buildPartialUpdatePayload(
} else {
payload.bookingWindow = {
type: currentState.rollingCalendarDays ? "calendarDays" : "businessDays",
value: parseInt(currentState.rollingDays),
value: parseInt(currentState.rollingDays, 10),
};
}
} else {
@@ -582,7 +647,8 @@ export function buildPartialUpdatePayload(
const originalRequiresConfirmation =
original.requiresConfirmation ||
(original.confirmationPolicy && !original.confirmationPolicy.disabled);
(original.confirmationPolicy &&
!("disabled" in original.confirmationPolicy && original.confirmationPolicy.disabled));
if (currentState.requiresConfirmation !== originalRequiresConfirmation) {
payload.requiresConfirmation = currentState.requiresConfirmation;
}
@@ -664,7 +730,7 @@ export function buildPartialUpdatePayload(
};
}
const metadataChanges: Record<string, any> = {};
const metadataChanges: Record<string, unknown> = {};
const originalMetadata = original.metadata || {};
if (
@@ -722,8 +788,8 @@ export function buildPartialUpdatePayload(
) {
if (currentState.recurringEnabled) {
payload.recurrence = {
interval: parseInt(currentState.recurringInterval) || 1,
occurrences: parseInt(currentState.recurringOccurrences) || 12,
interval: parseInt(currentState.recurringInterval, 10) || 1,
occurrences: parseInt(currentState.recurringOccurrences, 10) || 12,
frequency: currentState.recurringFrequency,
};
} else {
@@ -742,7 +808,7 @@ export function buildPartialUpdatePayload(
) {
if (currentState.seatsEnabled) {
payload.seats = {
seatsPerTimeSlot: parseInt(currentState.seatsPerTimeSlot) || 2,
seatsPerTimeSlot: parseInt(currentState.seatsPerTimeSlot, 10) || 2,
showAttendeeInfo: currentState.showAttendeeInfo,
showAvailabilityCount: currentState.showAvailabilityCount,
};
@@ -1,21 +1,21 @@
import { formatDuration } from "../../utils/formatters";
import { getEventDuration } from "../../utils/getEventDuration";
import { normalizeMarkdown } from "../../utils/normalizeMarkdown";
import { EventTypeListItemProps } from "./types";
import { Host, ContextMenu, Button, Image, HStack } from "@expo/ui/swift-ui";
import { Button, ContextMenu, Host, HStack, Image } from "@expo/ui/swift-ui";
import { buttonStyle, frame, padding } from "@expo/ui/swift-ui/modifiers";
import { Ionicons } from "@expo/vector-icons";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import React from "react";
import { View, Text, Pressable } from "react-native";
import type React from "react";
import { Pressable, Text, View } from "react-native";
import { formatDuration } from "../../utils/formatters";
import { getEventDuration } from "../../utils/getEventDuration";
import { normalizeMarkdown } from "../../utils/normalizeMarkdown";
import type { EventTypeListItemProps } from "./types";
export const EventTypeListItem = ({
item,
index,
filteredEventTypes,
copiedEventTypeId,
copiedEventTypeId: _copiedEventTypeId,
handleEventTypePress,
handleEventTypeLongPress,
handleEventTypeLongPress: _handleEventTypeLongPress,
handleCopyLink,
handlePreview,
onEdit,
@@ -1,12 +1,11 @@
import { Ionicons } from "@expo/vector-icons";
import { Text, TouchableOpacity, View } from "react-native";
import Svg, { Path } from "react-native-svg";
import { Tooltip } from "../../components/Tooltip";
import { formatDuration } from "../../utils/formatters";
import { getEventDuration } from "../../utils/getEventDuration";
import { normalizeMarkdown } from "../../utils/normalizeMarkdown";
import { EventTypeListItemProps } from "./types";
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { View, Text, TouchableOpacity } from "react-native";
import Svg, { Path } from "react-native-svg";
import type { EventTypeListItemProps } from "./types";
export const EventTypeListItem = ({
item,
@@ -1,4 +1,4 @@
import { EventType } from "../../services/types/event-types.types";
import type { EventType } from "../../services/types/event-types.types";
export interface EventTypeListItemProps {
item: EventType;
@@ -4,21 +4,22 @@
* Screen content for adding guests to a booking.
* Used with the add-guests route that has native Stack.Header.
*/
import type { Booking } from "../../services/calcom";
import { CalComAPIService } from "../../services/calcom";
import { Ionicons } from "@expo/vector-icons";
import React, { useState, useEffect, useCallback, forwardRef, useImperativeHandle } from "react";
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
import {
View,
Alert,
KeyboardAvoidingView,
Platform,
ScrollView,
Text,
TextInput,
TouchableOpacity,
Alert,
ScrollView,
KeyboardAvoidingView,
Platform,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import type { Booking } from "../../services/calcom";
import { CalComAPIService } from "../../services/calcom";
export interface AddGuestsScreenProps {
booking: Booking | null;
@@ -41,7 +42,7 @@ export const AddGuestsScreen = forwardRef<AddGuestsScreenHandle, AddGuestsScreen
const insets = useSafeAreaInsets();
const [email, setEmail] = useState("");
const [name, setName] = useState("");
const [guests, setGuests] = useState<Array<{ email: string; name?: string }>>([]);
const [guests, setGuests] = useState<{ email: string; name?: string }[]>([]);
const [isSaving, setIsSaving] = useState(false);
// Notify parent of saving state changes
@@ -184,7 +185,7 @@ export const AddGuestsScreen = forwardRef<AddGuestsScreenHandle, AddGuestsScreen
<View className="overflow-hidden rounded-xl bg-white">
{guests.map((guest, index) => (
<View
key={index}
key={guest.email}
className={`flex-row items-center px-4 py-3 ${
index < guests.length - 1 ? "border-b border-gray-100" : ""
}`}
@@ -1,14 +1,14 @@
import { CalComAPIService, Schedule } from "../../services/calcom";
import { ScheduleAvailability } from "../../services/types";
import { showErrorAlert } from "../../utils/alerts";
import { AppPressable } from "../AppPressable";
import { FullScreenModal } from "../FullScreenModal";
import { Ionicons } from "@expo/vector-icons";
import { GlassView } from "expo-glass-effect";
import { useRouter } from "expo-router";
import React, { useState, useEffect } from "react";
import { View, Text, ScrollView, TextInput, Alert, ActivityIndicator, Switch } from "react-native";
import { useCallback, useEffect, useState } from "react";
import { ActivityIndicator, Alert, ScrollView, Switch, Text, TextInput, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { CalComAPIService, type Schedule } from "../../services/calcom";
import type { ScheduleAvailability } from "../../services/types";
import { showErrorAlert } from "../../utils/alerts";
import { AppPressable } from "../AppPressable";
import { FullScreenModal } from "../FullScreenModal";
const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
const DAY_ABBREVIATIONS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
@@ -84,7 +84,8 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [schedule, setSchedule] = useState<Schedule | null>(null);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_schedule, setSchedule] = useState<Schedule | null>(null);
const [scheduleName, setScheduleName] = useState("");
const [timeZone, setTimeZone] = useState("");
const [isDefault, setIsDefault] = useState(false);
@@ -96,11 +97,11 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
type: "start" | "end";
} | null>(null);
const [overrides, setOverrides] = useState<
Array<{
{
date: string; // Format: "2024-05-20"
startTime: string; // Format: "12:00" or "00:00" for unavailable
endTime: string; // Format: "14:00" or "00:00" for unavailable
}>
}[]
>([]);
const [showOverrideModal, setShowOverrideModal] = useState(false);
const [editingOverride, setEditingOverride] = useState<number | null>(null);
@@ -128,13 +129,7 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
"UTC",
];
useEffect(() => {
if (id) {
fetchSchedule();
}
}, [id]);
const fetchSchedule = async () => {
const fetchSchedule = useCallback(async () => {
try {
setLoading(true);
const scheduleData = await CalComAPIService.getScheduleById(Number(id));
@@ -173,7 +168,7 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
// If it's a number string (e.g., "0", "1")
if (typeof day === "string") {
const parsed = parseInt(day, 10);
if (!isNaN(parsed) && parsed >= 0 && parsed <= 6) {
if (!Number.isNaN(parsed) && parsed >= 0 && parsed <= 6) {
return parsed;
}
}
@@ -225,7 +220,13 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
} finally {
setLoading(false);
}
};
}, [id, router]);
useEffect(() => {
if (id) {
fetchSchedule();
}
}, [id, fetchSchedule]);
const toggleDay = (dayIndex: number) => {
setAvailability((prev) => {
@@ -283,7 +284,7 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
) => {
setAvailability((prev) => {
const newAvailability = { ...prev };
if (newAvailability[dayIndex] && newAvailability[dayIndex][slotIndex]) {
if (newAvailability[dayIndex]?.[slotIndex]) {
const slot = { ...newAvailability[dayIndex][slotIndex] };
if (type === "start") {
slot.startTime = `${time}:00`;
@@ -301,11 +302,11 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
setSaving(true);
// Convert availability object back to array format with day names
const availabilityArray: Array<{
const availabilityArray: {
days: string[]; // Day names like "Monday", "Tuesday"
startTime: string; // Format: "09:00"
endTime: string; // Format: "10:00"
}> = [];
}[] = [];
Object.keys(availability).forEach((dayIndexStr) => {
const dayIndex = Number(dayIndexStr);
@@ -347,7 +348,7 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
Alert.alert("Success", "Schedule updated successfully", [
{ text: "OK", onPress: () => router.back() },
]);
} catch (error) {
} catch {
showErrorAlert("Error", "Failed to update schedule. Please try again.");
} finally {
setSaving(false);
@@ -361,7 +362,7 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
});
setIsDefault(true);
Alert.alert("Success", "Schedule set as default successfully");
} catch (error) {
} catch {
showErrorAlert("Error", "Failed to set schedule as default. Please try again.");
}
};
@@ -378,7 +379,7 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
Alert.alert("Success", "Schedule deleted successfully", [
{ text: "OK", onPress: () => router.back() },
]);
} catch (error) {
} catch {
showErrorAlert("Error", "Failed to delete schedule. Please try again.");
}
},
@@ -445,7 +446,7 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
const formatDateForDisplay = (dateStr: string): string => {
if (!dateStr) return "";
const date = new Date(dateStr + "T00:00:00");
const date = new Date(`${dateStr}T00:00:00`);
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
@@ -518,8 +519,8 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
<Text className="mb-3 text-xl font-bold text-[#333]">{scheduleName}</Text>
{Object.keys(availability).length > 0 ? (
<View>
{formatAvailabilityDisplay(availability).map((line, index) => (
<Text key={index} className="mb-1 text-base text-[#666]">
{formatAvailabilityDisplay(availability).map((line) => (
<Text key={line} className="mb-1 text-base text-[#666]">
{line}
</Text>
))}
@@ -539,7 +540,7 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
return (
<View
key={dayIndex}
key={day}
className={`mb-3 border-b border-[#E5E5EA] pb-3 ${
dayIndex === DAYS.length - 1 ? "mb-0 border-b-0 pb-0" : ""
}`}
@@ -597,7 +598,10 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
{isEnabled && daySlots.length > 1 ? (
<View className="mt-2" style={{ marginLeft: 108 }}>
{daySlots.slice(1).map((slot, slotIndex) => (
<View key={slotIndex + 1} className="mb-2 flex-row items-center gap-2">
<View
key={`${slotIndex}-${slot.startTime}-${slot.endTime}`}
className="mb-2 flex-row items-center gap-2"
>
<AppPressable
onPress={() =>
setShowTimePicker({
@@ -667,7 +671,7 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
<View className="mb-4">
{overrides.map((override, index) => (
<View
key={index}
key={override.date}
className="mb-2 flex-row items-center justify-between rounded-lg border border-[#E5E5EA] bg-[#F8F9FA] p-3"
>
<View className="flex-1">
@@ -678,8 +682,8 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
<Text className="text-sm text-[#666]">Unavailable (All day)</Text>
) : (
<Text className="text-sm text-[#666]">
{formatTime12Hour(override.startTime + ":00")} -{" "}
{formatTime12Hour(override.endTime + ":00")}
{formatTime12Hour(`${override.startTime}:00`)} -{" "}
{formatTime12Hour(`${override.endTime}:00`)}
</Text>
)}
</View>
@@ -925,7 +929,7 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
className="flex-1 rounded-lg border border-[#E5E5EA] bg-white px-3 py-3"
>
<Text className="text-center text-base text-[#333]">
{formatTime12Hour(overrideStartTime + ":00")}
{formatTime12Hour(`${overrideStartTime}:00`)}
</Text>
</AppPressable>
<Text className="text-base text-[#666]">-</Text>
@@ -934,7 +938,7 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
className="flex-1 rounded-lg border border-[#E5E5EA] bg-white px-3 py-3"
>
<Text className="text-center text-base text-[#333]">
{formatTime12Hour(overrideEndTime + ":00")}
{formatTime12Hour(`${overrideEndTime}:00`)}
</Text>
</AppPressable>
</View>
@@ -1003,7 +1007,7 @@ export function AvailabilityDetailScreen({ id }: AvailabilityDetailScreenProps)
isSelected ? "font-semibold text-[#007AFF]" : "text-[#333]"
}`}
>
{formatTime12Hour(time + ":00")}
{formatTime12Hour(`${time}:00`)}
</Text>
</AppPressable>
);
@@ -1,34 +1,34 @@
import { Ionicons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
import { Activity, useMemo, useState } from "react";
import {
ActionSheetIOS,
Alert,
FlatList,
Platform,
RefreshControl,
ScrollView,
Text,
TextInput,
View,
} from "react-native";
import { AppPressable } from "../../components/AppPressable";
import { AvailabilityListItem } from "../../components/availability-list-item/AvailabilityListItem";
import { EmptyScreen } from "../../components/EmptyScreen";
import { FullScreenModal } from "../../components/FullScreenModal";
import { Header } from "../../components/Header";
import { LoadingSpinner } from "../../components/LoadingSpinner";
import { AvailabilityListItem } from "../../components/availability-list-item/AvailabilityListItem";
import {
useSchedules,
useCreateSchedule,
useDeleteSchedule,
useDuplicateSchedule,
useSchedules,
useSetScheduleAsDefault,
} from "../../hooks";
import { CalComAPIService, Schedule } from "../../services/calcom";
import { CalComAPIService, type Schedule } from "../../services/calcom";
import { showErrorAlert } from "../../utils/alerts";
import { offlineAwareRefresh } from "../../utils/network";
import { shadows } from "../../utils/shadows";
import { Ionicons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
import React, { useState, useMemo, Activity } from "react";
import {
View,
Text,
FlatList,
RefreshControl,
ActionSheetIOS,
Alert,
Platform,
TextInput,
ScrollView,
} from "react-native";
export interface AvailabilityListScreenProps {
searchQuery: string;
@@ -97,11 +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: Array<{
const options: {
text: string;
onPress: () => void;
style?: "destructive" | "cancel" | "default";
}> = [];
}[] = [];
if (!schedule.isDefault) {
options.push({ text: "Set as default", onPress: () => handleSetAsDefault(schedule) });
}
@@ -234,7 +234,7 @@ export function AvailabilityListScreen({
if (userProfile.timeZone) {
userTimezone = userProfile.timeZone;
}
} catch (error) {
} catch {
console.log("Could not get user timezone, using default");
}
@@ -514,22 +514,20 @@ export function AvailabilityListScreen({
<Activity
mode={selectedSchedule && !selectedSchedule.isDefault ? "visible" : "hidden"}
>
<>
<AppPressable
onPress={() => {
setShowActionsModal(false);
if (selectedSchedule) {
handleSetAsDefault(selectedSchedule);
}
}}
className="flex-row items-center p-2 hover:bg-gray-50 md:p-4"
>
<Ionicons name="star-outline" size={20} color="#6B7280" />
<Text className="ml-3 text-base text-gray-900">Set as Default</Text>
</AppPressable>
<AppPressable
onPress={() => {
setShowActionsModal(false);
if (selectedSchedule) {
handleSetAsDefault(selectedSchedule);
}
}}
className="flex-row items-center p-2 hover:bg-gray-50 md:p-4"
>
<Ionicons name="star-outline" size={20} color="#6B7280" />
<Text className="ml-3 text-base text-gray-900">Set as Default</Text>
</AppPressable>
<View className="mx-4 my-2 h-px bg-gray-200" />
</>
<View className="mx-4 my-2 h-px bg-gray-200" />
</Activity>
{/* Duplicate */}
@@ -1,20 +1,18 @@
import { CalComAPIService, Booking } from "../../services/calcom";
import { Ionicons } from "@expo/vector-icons";
import { Stack, useRouter } from "expo-router";
import { useCallback, useEffect, useMemo, useState } from "react";
import { ActivityIndicator, Alert, Platform, ScrollView, Text, View } from "react-native";
import { useAuth } from "../../contexts/AuthContext";
import { useBookingActionModals } from "../../hooks";
import { type Booking, CalComAPIService } from "../../services/calcom";
import { showErrorAlert } from "../../utils/alerts";
import { getBookingActions, type BookingActionsResult } from "../../utils/booking-actions";
import { type BookingActionsResult, getBookingActions } from "../../utils/booking-actions";
import { openInAppBrowser } from "../../utils/browser";
import { getDefaultLocationIconUrl, defaultLocations } from "../../utils/defaultLocations";
import { defaultLocations, getDefaultLocationIconUrl } from "../../utils/defaultLocations";
import { formatAppIdToDisplayName } from "../../utils/formatters";
import { getAppIconUrl } from "../../utils/getAppIconUrl";
import { AppPressable } from "../AppPressable";
import { BookingActionsModal } from "../BookingActionsModal";
import { SvgImage } from "../SvgImage";
import { Ionicons } from "@expo/vector-icons";
import { Stack, useRouter } from "expo-router";
import React, { useState, useEffect, useMemo, useCallback } from "react";
import { View, Text, ScrollView, Alert, ActivityIndicator, Platform } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
// Empty actions result for when no booking is loaded
const EMPTY_ACTIONS: BookingActionsResult = {
@@ -39,7 +37,7 @@ const formatDateFull = (dateString: string): string => {
day: "numeric",
year: "numeric",
});
} catch (error) {
} catch {
return "";
}
};
@@ -55,7 +53,7 @@ const formatTime12Hour = (dateString: string): string => {
const hour12 = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
const minStr = minutes.toString().padStart(2, "0");
return `${hour12}:${minStr}${period}`;
} catch (error) {
} catch {
return "";
}
};
@@ -66,12 +64,12 @@ const getTimezone = (dateString: string): string => {
try {
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
return timeZone;
} catch (error) {
} catch {
return "";
}
};
// Get initials from a name (e.g., "Keith Williams" -> "KW", "Dhairyashil Shinde" -> "DS")
// Get initials from a name(e.g., "Keith Williams" -> "KW", "Dhairyashil Shinde" -> "DS")
const getInitials = (name: string): string => {
if (!name) return "";
const parts = name.trim().split(/\s+/);
@@ -190,7 +188,6 @@ export interface BookingDetailScreenProps {
export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreenProps) {
const router = useRouter();
const insets = useSafeAreaInsets();
const { userInfo } = useAuth();
const [loading, setLoading] = useState(true);
@@ -332,38 +329,7 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
});
}, [booking, router]);
useEffect(() => {
if (uid) {
fetchBooking();
}
}, [uid]);
// Expose action handlers to parent component (for iOS header menu)
useEffect(() => {
if (booking && onActionsReady) {
onActionsReady({
openRescheduleModal,
openEditLocationModal,
openAddGuestsModal,
openViewRecordingsModal,
openMeetingSessionDetailsModal,
openMarkNoShowModal,
handleCancelBooking,
});
}
}, [
booking,
onActionsReady,
openRescheduleModal,
openEditLocationModal,
openAddGuestsModal,
openViewRecordingsModal,
openMeetingSessionDetailsModal,
handleCancelBooking,
openMarkNoShowModal,
]);
const fetchBooking = async () => {
const fetchBooking = useCallback(async () => {
try {
setLoading(true);
setError(null);
@@ -397,7 +363,38 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
} finally {
setLoading(false);
}
};
}, [uid, router]);
useEffect(() => {
if (uid) {
fetchBooking();
}
}, [uid, fetchBooking]);
// Expose action handlers to parent component (for iOS header menu)
useEffect(() => {
if (booking && onActionsReady) {
onActionsReady({
openRescheduleModal,
openEditLocationModal,
openAddGuestsModal,
openViewRecordingsModal,
openMeetingSessionDetailsModal,
openMarkNoShowModal,
handleCancelBooking,
});
}
}, [
booking,
onActionsReady,
openRescheduleModal,
openEditLocationModal,
openAddGuestsModal,
openViewRecordingsModal,
openMeetingSessionDetailsModal,
handleCancelBooking,
openMarkNoShowModal,
]);
const handleJoinMeeting = () => {
if (!booking?.location) return;
@@ -493,7 +490,7 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
) : booking.hosts && booking.hosts.length > 0 ? (
booking.hosts.map((host, hostIndex) => (
<View
key={hostIndex}
key={host.email ?? host.name}
className={`flex-row items-start ${hostIndex > 0 ? "mt-4" : ""}`}
>
<View className="mr-3 h-12 w-12 items-center justify-center rounded-full bg-black">
@@ -521,9 +518,13 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
<View>
{booking.attendees.map((attendee, index) => {
const isNoShow =
(attendee as any).noShow === true || (attendee as any).absent === true;
(attendee as { noShow?: boolean; absent?: boolean }).noShow === true ||
(attendee as { noShow?: boolean; absent?: boolean }).absent === true;
return (
<View key={index} className={`flex-row items-start ${index > 0 ? "mt-4" : ""}`}>
<View
key={attendee.email}
className={`flex-row items-start ${index > 0 ? "mt-4" : ""}`}
>
<View
className={`mr-3 h-12 w-12 items-center justify-center rounded-full ${
isNoShow ? "bg-[#DC2626]" : "bg-black"
@@ -4,15 +4,16 @@
* iOS-specific implementation with native context menu for location type selection.
* Uses @expo/ui/swift-ui ContextMenu for the glass UI feel.
*/
import { Button, ContextMenu, Host, HStack, Image } from "@expo/ui/swift-ui";
import { buttonStyle, frame, padding } from "@expo/ui/swift-ui/modifiers";
import { Ionicons } from "@expo/vector-icons";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
import { Alert, KeyboardAvoidingView, ScrollView, Text, TextInput, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import type { Booking } from "../../services/calcom";
import { CalComAPIService } from "../../services/calcom";
import { Ionicons } from "@expo/vector-icons";
import { Host, ContextMenu, Button, Image, HStack } from "@expo/ui/swift-ui";
import { buttonStyle, frame, padding } from "@expo/ui/swift-ui/modifiers";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import React, { useState, useEffect, useCallback, forwardRef, useImperativeHandle } from "react";
import { View, Text, TextInput, Alert, ScrollView, KeyboardAvoidingView } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
// Location types configuration
export const LOCATION_TYPES = {
@@ -148,7 +149,6 @@ export const EditLocationScreen = forwardRef<EditLocationScreenHandle, EditLocat
case "phone":
locationPayload = { type: "phone", phone: trimmedValue };
break;
case "address":
default:
locationPayload = { type: "address", address: trimmedValue };
}
@@ -13,23 +13,24 @@
* Integration-based locations (Cal Video, Google Meet, Zoom) are NOT
* supported for updating existing bookings via the current API.
*/
import type { Booking } from "../../services/calcom";
import { CalComAPIService } from "../../services/calcom";
import { Ionicons } from "@expo/vector-icons";
import React, { useState, useEffect, useCallback, forwardRef, useImperativeHandle } from "react";
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
import {
View,
Alert,
FlatList,
KeyboardAvoidingView,
Modal,
Platform,
ScrollView,
Text,
TextInput,
Alert,
ScrollView,
KeyboardAvoidingView,
Platform,
TouchableOpacity,
Modal,
FlatList,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import type { Booking } from "../../services/calcom";
import { CalComAPIService } from "../../services/calcom";
export const LOCATION_TYPES = {
link: {
@@ -151,7 +152,6 @@ export const EditLocationScreen = forwardRef<EditLocationScreenHandle, EditLocat
case "phone":
locationPayload = { type: "phone", phone: trimmedValue };
break;
case "address":
default:
locationPayload = { type: "address", address: trimmedValue };
}
@@ -4,12 +4,13 @@
* Screen content for marking attendees as no-show for a booking.
* Used with the mark-no-show route that has native Stack.Header.
*/
import { Ionicons } from "@expo/vector-icons";
import { useState } from "react";
import { ActivityIndicator, Alert, FlatList, Text, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import type { Booking } from "../../services/calcom";
import { CalComAPIService } from "../../services/calcom";
import { Ionicons } from "@expo/vector-icons";
import React, { useState } from "react";
import { View, Text, TouchableOpacity, ActivityIndicator, FlatList, Alert } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
interface Attendee {
id?: number | string;
@@ -80,14 +81,22 @@ export function MarkNoShowScreen({
// API returns "absent" field, not "noShow"
const updatedAttendees: Attendee[] = [];
if (updatedBooking.attendees && Array.isArray(updatedBooking.attendees)) {
updatedBooking.attendees.forEach((att: any) => {
updatedAttendees.push({
id: att.id,
email: att.email,
name: att.name || att.email,
noShow: att.absent === true || att.noShow === true,
});
});
updatedBooking.attendees.forEach(
(att: {
id?: number | string;
email: string;
name?: string;
noShow?: boolean;
absent?: boolean;
}) => {
updatedAttendees.push({
id: att.id,
email: att.email,
name: att.name || att.email,
noShow: att.absent === true || att.noShow === true,
});
}
);
}
onUpdate(updatedAttendees);
@@ -4,11 +4,11 @@
* Screen content for viewing meeting session details of a Cal Video booking.
* Used with the meeting-session-details route that has native Stack.Header.
*/
import type { ConferencingSession } from "../../services/types/bookings.types";
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { View, Text, FlatList } from "react-native";
import { FlatList, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import type { ConferencingSession } from "../../services/types/bookings.types";
export interface MeetingSessionDetailsScreenProps {
sessions: ConferencingSession[];
@@ -4,22 +4,23 @@
* Screen content for rescheduling a booking.
* Used with the reschedule route that has native Stack.Header.
*/
import type { Booking } from "../../services/calcom";
import { CalComAPIService } from "../../services/calcom";
import { Ionicons } from "@expo/vector-icons";
import React, { useState, useEffect, useCallback, forwardRef, useImperativeHandle } from "react";
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
import {
View,
Alert,
KeyboardAvoidingView,
Modal,
Platform,
ScrollView,
Text,
TextInput,
Alert,
ScrollView,
KeyboardAvoidingView,
Platform,
TouchableOpacity,
Modal,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import type { Booking } from "../../services/calcom";
import { CalComAPIService } from "../../services/calcom";
// Note: @expo/ui DateTimePicker components are not yet stable
// Using a simple inline picker approach instead
@@ -48,7 +49,7 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
useEffect(() => {
if (booking?.startTime) {
const bookingDate = new Date(booking.startTime);
if (!isNaN(bookingDate.getTime())) {
if (!Number.isNaN(bookingDate.getTime())) {
setSelectedDateTime(bookingDate);
setReason("");
}
@@ -103,26 +104,10 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
hour12: false,
});
// Expose submit function to parent via ref (same pattern as senior's actionHandlersRef)
useImperativeHandle(
ref,
() => ({
submit: handleSubmit,
}),
[handleSubmit]
);
if (!booking) {
return (
<View className="flex-1 items-center justify-center bg-[#F2F2F7]">
<Text className="text-gray-500">No booking data</Text>
</View>
);
}
// Generate date options for picker (next 90 days)
// Note: useMemo must be called before any conditional returns to follow React hooks rules
const dateOptions = React.useMemo(() => {
const options: Array<{ label: string; value: Date }> = [];
const options: { label: string; value: Date }[] = [];
const today = new Date();
for (let i = 0; i < 90; i++) {
const date = new Date(today);
@@ -141,7 +126,7 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
// Generate time options (every 15 minutes)
const timeOptions = React.useMemo(() => {
const options: Array<{ label: string; value: { hour: number; minute: number } }> = [];
const options: { 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")}`;
@@ -154,6 +139,23 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
return options;
}, []);
// Expose submit function to parent via ref (same pattern as senior's actionHandlersRef)
useImperativeHandle(
ref,
() => ({
submit: handleSubmit,
}),
[handleSubmit]
);
if (!booking) {
return (
<View className="flex-1 items-center justify-center bg-[#F2F2F7]">
<Text className="text-gray-500">No booking data</Text>
</View>
);
}
return (
<>
<KeyboardAvoidingView
@@ -268,12 +270,12 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingVertical: 4 }}
>
{dateOptions.map((option, index) => {
{dateOptions.map((option) => {
const isSelected =
option.value.toDateString() === selectedDateTime.toDateString();
return (
<TouchableOpacity
key={index}
key={option.value.toISOString()}
className={`px-4 py-3 ${isSelected ? "bg-[#E8F4FD]" : ""}`}
onPress={() => {
const newDate = new Date(option.value);
@@ -330,13 +332,13 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingVertical: 4 }}
>
{timeOptions.map((option, index) => {
{timeOptions.map((option) => {
const isSelected =
selectedDateTime.getHours() === option.value.hour &&
selectedDateTime.getMinutes() === option.value.minute;
return (
<TouchableOpacity
key={index}
key={option.label}
className={`px-4 py-3 ${isSelected ? "bg-[#E8F4FD]" : ""}`}
onPress={() => {
const newDate = new Date(selectedDateTime);
@@ -4,12 +4,12 @@
* Screen content for viewing recordings of a Cal Video booking.
* Used with the view-recordings route that has native Stack.Header.
*/
import type { BookingRecording } from "../../services/types/bookings.types";
import { Ionicons } from "@expo/vector-icons";
import * as WebBrowser from "expo-web-browser";
import React from "react";
import { View, Text, TouchableOpacity, FlatList } from "react-native";
import { FlatList, Text, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import type { BookingRecording } from "../../services/types/bookings.types";
export interface ViewRecordingsScreenProps {
recordings: BookingRecording[];
+1 -1
View File
@@ -12,7 +12,7 @@ const getEnvNumber = (key: string, fallback: number): number => {
return fallback;
}
const parsed = parseInt(value, 10);
return isNaN(parsed) ? fallback : parsed;
return Number.isNaN(parsed) ? fallback : parsed;
};
// Convert minutes to milliseconds
+112 -106
View File
@@ -1,13 +1,13 @@
import { createContext, type ReactNode, useCallback, useContext, useEffect, useState } from "react";
import { CalComAPIService } from "../services/calcom";
import {
type CalComOAuthService,
createCalComOAuthService,
OAuthTokens,
CalComOAuthService,
type OAuthTokens,
} 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
@@ -69,7 +69,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
});
// Setup refresh token function for OAuth
const setupRefreshTokenFunction = (service: CalComOAuthService) => {
const setupRefreshTokenFunction = useCallback((service: CalComOAuthService) => {
CalComAPIService.setRefreshTokenFunction(async (refreshToken: string) => {
const newTokens = await service.refreshAccessToken(refreshToken);
return {
@@ -77,10 +77,10 @@ export function AuthProvider({ children }: AuthProviderProps) {
refreshToken: newTokens.refreshToken,
};
});
};
}, []);
// Common post-login setup: configure API service and fetch user profile
const setupAfterLogin = async (token: string, refreshToken?: string) => {
const setupAfterLogin = useCallback(async (token: string, refreshToken?: string) => {
CalComAPIService.setAccessToken(token, refreshToken);
try {
@@ -98,22 +98,25 @@ export function AuthProvider({ children }: AuthProviderProps) {
console.error("Failed to fetch user profile:", profileError);
// Don't fail login if profile fetch fails
}
};
}, []);
const saveOAuthTokens = async (tokens: OAuthTokens) => {
await storage.set(OAUTH_TOKENS_KEY, JSON.stringify(tokens));
await storage.set(AUTH_TYPE_KEY, "oauth");
const saveOAuthTokens = useCallback(
async (tokens: OAuthTokens) => {
await storage.set(OAUTH_TOKENS_KEY, JSON.stringify(tokens));
await storage.set(AUTH_TYPE_KEY, "oauth");
if (oauthService) {
try {
await oauthService.syncTokensToExtension(tokens);
} catch (error) {
console.warn("Failed to sync tokens to extension:", error);
if (oauthService) {
try {
await oauthService.syncTokensToExtension(tokens);
} catch (error) {
console.warn("Failed to sync tokens to extension:", error);
}
}
}
};
},
[oauthService]
);
const clearAuth = async () => {
const clearAuth = useCallback(async () => {
await storage.removeAll([ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, OAUTH_TOKENS_KEY, AUTH_TYPE_KEY]);
if (oauthService) {
@@ -123,10 +126,10 @@ export function AuthProvider({ children }: AuthProviderProps) {
console.warn("Failed to clear tokens from extension:", error);
}
}
};
}, [oauthService]);
// Reset all auth state
const resetAuthState = () => {
const resetAuthState = useCallback(() => {
setAccessToken(null);
setRefreshToken(null);
setUserInfo(null);
@@ -134,7 +137,94 @@ export function AuthProvider({ children }: AuthProviderProps) {
setIsWebSession(false);
CalComAPIService.clearAuth();
CalComAPIService.clearUserProfile();
};
}, []);
const logout = useCallback(async () => {
try {
await clearAuth();
resetAuthState();
} catch (error) {
const message = getErrorMessage(error);
console.error("Failed to logout", message);
if (__DEV__) {
console.debug("[AuthContext] logout failed", { message, stack: getErrorStack(error) });
}
}
}, [clearAuth, resetAuthState]);
// Handle OAuth authentication
const handleOAuthAuth = useCallback(
async (storedTokens: OAuthTokens) => {
if (!oauthService) return;
// Refresh token if expired
let tokens = storedTokens;
let tokensWereRefreshed = false;
if (oauthService.isTokenExpired(storedTokens) && storedTokens.refreshToken) {
try {
console.log("Access token expired, refreshing...");
tokens = await oauthService.refreshAccessToken(storedTokens.refreshToken);
await saveOAuthTokens(tokens);
tokensWereRefreshed = true;
} catch (refreshError) {
console.error("Failed to refresh token:", refreshError);
await clearAuth();
return;
}
}
// Set state
setAccessToken(tokens.accessToken);
setRefreshToken(tokens.refreshToken || null);
setIsAuthenticated(true);
setIsWebSession(false);
// Setup API service and refresh function
await setupAfterLogin(tokens.accessToken, tokens.refreshToken);
if (tokens.refreshToken) {
setupRefreshTokenFunction(oauthService);
}
if (!tokensWereRefreshed) {
try {
await oauthService.syncTokensToExtension(tokens);
} catch (error) {
console.warn("Failed to sync tokens to extension on init:", error);
}
}
},
[oauthService, saveOAuthTokens, clearAuth, setupAfterLogin, setupRefreshTokenFunction]
);
// Handle web session authentication
const handleWebSessionAuth = useCallback(() => {
setIsWebSession(true);
}, []);
const checkAuthState = useCallback(async () => {
try {
const authType = (await storage.get(AUTH_TYPE_KEY)) as AuthType | null;
const storedOAuthTokens = await storage.get(OAUTH_TOKENS_KEY);
const storedTokens = storedOAuthTokens ? JSON.parse(storedOAuthTokens) : null;
if (authType === "oauth" && storedTokens && oauthService) {
await handleOAuthAuth(storedTokens);
} else if (authType === "web_session") {
handleWebSessionAuth();
}
} catch (error) {
const message = getErrorMessage(error);
console.error("Failed to check auth state", message);
if (__DEV__) {
console.debug("[AuthContext] checkAuthState failed", {
message,
stack: getErrorStack(error),
});
}
} finally {
setLoading(false);
}
}, [oauthService, handleOAuthAuth, handleWebSessionAuth]);
useEffect(() => {
checkAuthState();
@@ -176,78 +266,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
return () => {
CalComAPIService.setTokenRefreshCallback(() => Promise.resolve());
};
}, [refreshToken]);
// Handle OAuth authentication
const handleOAuthAuth = async (storedTokens: OAuthTokens) => {
if (!oauthService) return;
// Refresh token if expired
let tokens = storedTokens;
let tokensWereRefreshed = false;
if (oauthService.isTokenExpired(storedTokens) && storedTokens.refreshToken) {
try {
console.log("Access token expired, refreshing...");
tokens = await oauthService.refreshAccessToken(storedTokens.refreshToken);
await saveOAuthTokens(tokens);
tokensWereRefreshed = true;
} catch (refreshError) {
console.error("Failed to refresh token:", refreshError);
await clearAuth();
return;
}
}
// Set state
setAccessToken(tokens.accessToken);
setRefreshToken(tokens.refreshToken || null);
setIsAuthenticated(true);
setIsWebSession(false);
// Setup API service and refresh function
await setupAfterLogin(tokens.accessToken, tokens.refreshToken);
if (tokens.refreshToken) {
setupRefreshTokenFunction(oauthService);
}
if (!tokensWereRefreshed) {
try {
await oauthService.syncTokensToExtension(tokens);
} catch (error) {
console.warn("Failed to sync tokens to extension on init:", error);
}
}
};
// Handle web session authentication
const handleWebSessionAuth = () => {
setIsWebSession(true);
};
const checkAuthState = async () => {
try {
const authType = (await storage.get(AUTH_TYPE_KEY)) as AuthType | null;
const storedOAuthTokens = await storage.get(OAUTH_TOKENS_KEY);
const storedTokens = storedOAuthTokens ? JSON.parse(storedOAuthTokens) : null;
if (authType === "oauth" && storedTokens && oauthService) {
await handleOAuthAuth(storedTokens);
} else if (authType === "web_session") {
handleWebSessionAuth();
}
} catch (error) {
const message = getErrorMessage(error);
console.error("Failed to check auth state", message);
if (__DEV__) {
console.debug("[AuthContext] checkAuthState failed", {
message,
stack: getErrorStack(error),
});
}
} finally {
setLoading(false);
}
};
}, [refreshToken, checkAuthState, logout, saveOAuthTokens]);
const loginFromWebSession = async (sessionUserInfo: UserProfile) => {
try {
@@ -323,19 +342,6 @@ export function AuthProvider({ children }: AuthProviderProps) {
}
};
const logout = async () => {
try {
await clearAuth();
resetAuthState();
} catch (error) {
const message = getErrorMessage(error);
console.error("Failed to logout", message);
if (__DEV__) {
console.debug("[AuthContext] logout failed", { message, stack: getErrorStack(error) });
}
}
};
const value: AuthContextType = {
isAuthenticated,
accessToken,
+4 -4
View File
@@ -8,12 +8,12 @@
* - Cross-platform compatibility (mobile + extension)
*/
import React, { ReactNode, useState, useEffect, useCallback, useMemo } from "react";
import { Platform } from "react-native";
import { QueryClient, QueryClientProvider, onlineManager } from "@tanstack/react-query";
import { onlineManager, QueryClient } from "@tanstack/react-query";
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
import React, { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import { Platform } from "react-native";
import { CACHE_CONFIG } from "../config/cache.config";
import { createQueryPersister, clearQueryCache } from "../utils/queryPersister";
import { clearQueryCache, createQueryPersister } from "../utils/queryPersister";
/**
* Create and configure the QueryClient instance
@@ -165,7 +165,7 @@ function openAppPage(): void {
}
export default defineBackground(() => {
const browserType = detectBrowser();
const _browserType = detectBrowser();
const browserName = getBrowserDisplayName();
if (IS_DEV_MODE) {
@@ -199,7 +199,7 @@ export default defineBackground(() => {
}
if (runtimeAPI) {
runtimeAPI.onMessage.addListener((message, sender, sendResponse) => {
runtimeAPI.onMessage.addListener((message, _sender, sendResponse) => {
if (message.action === "fetch-event-types") {
fetchEventTypes()
.then((eventTypes) => sendResponse({ data: eventTypes }))
@@ -375,7 +375,7 @@ async function handleExtensionOAuth(authUrl: string): Promise<string> {
const result = identityAPI.launchWebAuthFlow({
url: authUrl,
interactive: true,
}) as Promise<string | undefined> | void;
}) as Promise<string | undefined> | undefined;
if (result && typeof result.then === "function") {
result
+285 -94
View File
@@ -206,7 +206,7 @@ export default defineContentScript({
// Handle token exchange requests by forwarding to background script
function handleTokenExchangeRequest(
tokenRequest: any,
tokenRequest: Record<string, string>,
tokenEndpoint: string,
state: string | undefined,
iframeWindow: Window | null
@@ -260,7 +260,10 @@ export default defineContentScript({
);
}
function handleTokenSyncRequest(tokens: any, iframeWindow: Window | null) {
function handleTokenSyncRequest(
tokens: { accessToken?: string; refreshToken?: string; expiresAt?: number },
iframeWindow: Window | null
) {
chrome.runtime.sendMessage({ action: "sync-oauth-tokens", tokens }, (response) => {
if (chrome.runtime.lastError) {
console.error(
@@ -521,7 +524,7 @@ export default defineContentScript({
}
// Listen for extension icon click
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
// Skip debug-log messages to avoid infinite loop
if (message.action === "icon-clicked") {
if (isClosed) {
@@ -586,7 +589,13 @@ export default defineContentScript({
// Gmail integration function
function initGmailIntegration() {
// Cache for event types (refreshed on page reload)
let eventTypesCache: any[] | null = null;
let eventTypesCache: Array<{
id: number;
title: string;
slug: string;
lengthInMinutes?: number;
description?: string;
}> | null = null;
let cacheTimestamp: number | null = null;
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
@@ -718,7 +727,9 @@ export default defineContentScript({
document.addEventListener("click", function closeMenu(e) {
if (!menu.contains(e.target as Node)) {
// Clean up all tooltips
tooltipsToCleanup.forEach((tooltip) => tooltip.remove());
for (const tooltip of tooltipsToCleanup) {
tooltip.remove();
}
menu.remove();
document.removeEventListener("click", closeMenu);
}
@@ -774,7 +785,7 @@ export default defineContentScript({
return tooltip;
}
function openCalSidebar() {
function _openCalSidebar() {
// Open Cal.com sidebar or quick schedule flow
if (isClosed) {
// Trigger sidebar open
@@ -800,11 +811,20 @@ export default defineContentScript({
const isCacheValid =
eventTypesCache && cacheTimestamp && now - cacheTimestamp < CACHE_DURATION;
let eventTypes: any[] = [];
let eventTypes: Array<{
id: number;
title: string;
slug: string;
lengthInMinutes?: number;
length?: number;
duration?: number;
description?: string;
users?: Array<{ username?: string }>;
}> = [];
if (isCacheValid) {
if (isCacheValid && eventTypesCache) {
// Use cached data
eventTypes = eventTypesCache!;
eventTypes = eventTypesCache;
} else {
// Check if extension context is valid
if (!chrome.runtime?.id) {
@@ -817,7 +837,7 @@ export default defineContentScript({
chrome.runtime.sendMessage({ action: "fetch-event-types" }, (response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else if (response && response.error) {
} else if (response?.error) {
reject(new Error(response.error));
} else {
resolve(response);
@@ -828,8 +848,37 @@ export default defineContentScript({
}
});
if (response && (response as any).data) {
eventTypes = (response as any).data;
if (
response &&
(
response as {
data?: Array<{
id: number;
title: string;
slug: string;
lengthInMinutes?: number;
length?: number;
duration?: number;
description?: string;
users?: Array<{ username?: string }>;
}>;
}
).data
) {
eventTypes = (
response as {
data: Array<{
id: number;
title: string;
slug: string;
lengthInMinutes?: number;
length?: number;
duration?: number;
description?: string;
users?: Array<{ username?: string }>;
}>;
}
).data;
} else if (Array.isArray(response)) {
eventTypes = response;
} else {
@@ -945,7 +994,7 @@ export default defineContentScript({
tooltipsToCleanup.push(contentTooltip);
// Show/hide tooltip
contentWrapper.addEventListener("mouseenter", (e) => {
contentWrapper.addEventListener("mouseenter", (_e) => {
const rect = contentWrapper.getBoundingClientRect();
// Shift right to better center on the visible content
contentTooltip.style.left = `${rect.left + rect.width / 2 + 80}px`;
@@ -1147,7 +1196,7 @@ export default defineContentScript({
menu.appendChild(menuItem);
});
} catch (forEachError) {
} catch (_forEachError) {
menu.innerHTML = `
<div style="padding: 16px; text-align: center; color: #ea4335;">
Error displaying event types
@@ -1164,7 +1213,9 @@ export default defineContentScript({
if (isAuthError) {
// Not logged in - open sidebar directly
tooltipsToCleanup.forEach((tooltip) => tooltip.remove());
for (const tooltip of tooltipsToCleanup) {
tooltip.remove();
}
menu.remove();
openSidebar();
return;
@@ -1208,7 +1259,9 @@ export default defineContentScript({
closeBtn.style.background = "#F3F4F6";
});
closeBtn.addEventListener("click", () => {
tooltipsToCleanup.forEach((tooltip) => tooltip.remove());
for (const tooltip of tooltipsToCleanup) {
tooltip.remove();
}
menu.remove();
});
}
@@ -1243,7 +1296,7 @@ export default defineContentScript({
}
}
function copyEventTypeLink(eventType: {
function _copyEventTypeLink(eventType: {
slug: string;
users?: Array<{ username?: string }>;
}): void {
@@ -1289,7 +1342,7 @@ export default defineContentScript({
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
// If no selection, append to the end
const textNode = document.createTextNode(" " + text + " ");
const textNode = document.createTextNode(` ${text} `);
composeBody.appendChild(textNode);
// Move cursor after inserted text
@@ -1307,7 +1360,7 @@ export default defineContentScript({
range.deleteContents();
// Create a text node with the link (with spaces around it)
const textNode = document.createTextNode(" " + text + " ");
const textNode = document.createTextNode(` ${text} `);
range.insertNode(textNode);
// Move cursor after inserted text
@@ -1492,18 +1545,27 @@ export default defineContentScript({
}
}
async function fetchEventTypes(menu: HTMLElement, tooltipsToCleanup: HTMLElement[]) {
async function _fetchEventTypes(menu: HTMLElement, tooltipsToCleanup: HTMLElement[]) {
try {
// Check cache first
const now = Date.now();
const isCacheValid =
eventTypesCache && cacheTimestamp && now - cacheTimestamp < CACHE_DURATION;
let eventTypes: any[] = [];
let eventTypes: Array<{
id: number;
title: string;
slug: string;
lengthInMinutes?: number;
length?: number;
duration?: number;
description?: string;
users?: Array<{ username?: string }>;
}> = [];
if (isCacheValid) {
if (isCacheValid && eventTypesCache) {
// Use cached data
eventTypes = eventTypesCache!;
eventTypes = eventTypesCache;
} else {
// Check if extension context is valid
if (!chrome.runtime?.id) {
@@ -1516,7 +1578,7 @@ export default defineContentScript({
chrome.runtime.sendMessage({ action: "fetch-event-types" }, (response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else if (response && response.error) {
} else if (response?.error) {
reject(new Error(response.error));
} else {
resolve(response);
@@ -1527,8 +1589,37 @@ export default defineContentScript({
}
});
if (response && (response as any).data) {
eventTypes = (response as any).data;
if (
response &&
(
response as {
data?: Array<{
id: number;
title: string;
slug: string;
lengthInMinutes?: number;
length?: number;
duration?: number;
description?: string;
users?: Array<{ username?: string }>;
}>;
}
).data
) {
eventTypes = (
response as {
data: Array<{
id: number;
title: string;
slug: string;
lengthInMinutes?: number;
length?: number;
duration?: number;
description?: string;
users?: Array<{ username?: string }>;
}>;
}
).data;
} else if (Array.isArray(response)) {
eventTypes = response;
} else {
@@ -1644,7 +1735,7 @@ export default defineContentScript({
tooltipsToCleanup.push(contentTooltip);
// Show/hide tooltip
contentWrapper.addEventListener("mouseenter", (e) => {
contentWrapper.addEventListener("mouseenter", (_e) => {
const rect = contentWrapper.getBoundingClientRect();
contentTooltip.style.left = `${rect.left + rect.width / 2 + 80}px`;
contentTooltip.style.top = `${rect.top + 35}px`;
@@ -1845,7 +1936,7 @@ export default defineContentScript({
menu.appendChild(menuItem);
});
} catch (forEachError) {
} catch (_forEachError) {
menu.innerHTML = `
<div style="padding: 16px; text-align: center; color: #ea4335;">
Error displaying event types
@@ -1862,7 +1953,9 @@ export default defineContentScript({
if (isAuthError) {
// Not logged in - open sidebar directly
tooltipsToCleanup.forEach((tooltip) => tooltip.remove());
for (const tooltip of tooltipsToCleanup) {
tooltip.remove();
}
menu.remove();
openCalSidebar();
return;
@@ -1906,7 +1999,9 @@ export default defineContentScript({
closeBtn.style.background = "#F3F4F6";
});
closeBtn.addEventListener("click", () => {
tooltipsToCleanup.forEach((tooltip) => tooltip.remove());
for (const tooltip of tooltipsToCleanup) {
tooltip.remove();
}
menu.remove();
});
}
@@ -1914,7 +2009,10 @@ export default defineContentScript({
}
}
function insertEventTypeLink(eventType: any) {
function insertEventTypeLink(eventType: {
slug: string;
users?: Array<{ username?: string }>;
}) {
// Construct the Cal.com booking link
const bookingUrl = `https://cal.com/${eventType.users?.[0]?.username || "user"}/${
eventType.slug
@@ -1959,7 +2057,7 @@ export default defineContentScript({
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
// If no selection, append to the end
const textNode = document.createTextNode(" " + text + " ");
const textNode = document.createTextNode(` ${text} `);
messageInput.appendChild(textNode);
// Move cursor after inserted text
@@ -1977,7 +2075,7 @@ export default defineContentScript({
range.deleteContents();
// Create a text node with the link (with spaces around it)
const textNode = document.createTextNode(" " + text + " ");
const textNode = document.createTextNode(` ${text} `);
range.insertNode(textNode);
// Move cursor after inserted text
@@ -2062,9 +2160,16 @@ export default defineContentScript({
* Based on the email embed feature in main Cal.com codebase
*/
function generateEmailEmbedHTML(params: {
eventType: any;
eventType: { slug: string; title?: string };
username: string;
slots: any[];
slots: Array<{
isoDate: string;
date: string;
isoTimestamp: string;
time?: string;
startTime?: string;
endTime?: string;
}>;
duration: number;
timezone: string;
timezoneOffset: string;
@@ -2072,7 +2177,16 @@ export default defineContentScript({
const { eventType, username, slots, duration, timezone, timezoneOffset } = params;
// Group slots by date
const slotsByDate: { [date: string]: any[] } = {};
const slotsByDate: {
[date: string]: Array<{
isoDate: string;
date: string;
isoTimestamp: string;
time?: string;
startTime?: string;
endTime?: string;
}>;
} = {};
slots.forEach((slot) => {
if (!slotsByDate[slot.isoDate]) {
slotsByDate[slot.isoDate] = [];
@@ -2261,7 +2375,7 @@ export default defineContentScript({
console.warn("Cal.com: Failed to insert HTML at cursor:", insertError);
return false;
}
} catch (error) {
} catch (_error) {
console.error("Cal.com: Critical error inserting HTML");
return false;
}
@@ -2324,7 +2438,7 @@ export default defineContentScript({
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
try {
const textNode = document.createTextNode(" " + text + " ");
const textNode = document.createTextNode(` ${text} `);
composeBody.appendChild(textNode);
const range = document.createRange();
range.setStartAfter(textNode);
@@ -2341,7 +2455,7 @@ export default defineContentScript({
try {
const range = selection.getRangeAt(0);
range.deleteContents();
const textNode = document.createTextNode(" " + text + " ");
const textNode = document.createTextNode(` ${text} `);
range.insertNode(textNode);
range.setStartAfter(textNode);
range.collapse(true);
@@ -2414,7 +2528,7 @@ export default defineContentScript({
try {
notification.style.opacity = "1";
notification.style.transform = "translateY(0)";
} catch (e) {
} catch (_e) {
// Ignore animation errors
}
});
@@ -2426,11 +2540,11 @@ export default defineContentScript({
setTimeout(() => {
try {
notification.remove();
} catch (e) {
} catch (_e) {
// Ignore removal errors
}
}, 200);
} catch (e) {
} catch (_e) {
// Ignore animation errors
}
}, 3000);
@@ -2445,7 +2559,7 @@ export default defineContentScript({
*/
function watchForGoogleChips() {
try {
const observer = new MutationObserver((mutations) => {
const observer = new MutationObserver((_mutations) => {
try {
const chips = document.querySelectorAll(
".gmail_chip.gmail_ad_hoc_v2_content:not([data-calcom-chip-processed])"
@@ -2489,7 +2603,7 @@ export default defineContentScript({
console.warn("Cal.com: Failed to find existing chips:", error);
}
}, 1000);
} catch (error) {
} catch (_error) {
// Critical failure - log but don't break Gmail
console.error("Cal.com: Failed to initialize chip watcher");
}
@@ -2557,7 +2671,7 @@ export default defineContentScript({
console.log(`Cal.com: 🔄 Chip updated - ${parsedData.detectedDuration}min`);
try {
existingActionBar.remove();
} catch (e) {
} catch (_e) {
// Silently ignore removal errors
}
}
@@ -2578,11 +2692,11 @@ export default defineContentScript({
actionBar?.remove();
// Recreate action bar with new data
handleGoogleChipDetected(chipElement);
} catch (e) {
} catch (_e) {
// Silently ignore recreation errors
}
}
} catch (error) {
} catch (_error) {
// Silently ignore observer errors - expected during DOM updates
}
});
@@ -2594,7 +2708,7 @@ export default defineContentScript({
attributes: true,
attributeFilter: ["data-ad-hoc-v2-params"],
});
} catch (error) {
} catch (_error) {
// Silently ignore observer setup errors
}
@@ -2758,11 +2872,28 @@ export default defineContentScript({
embedButton.style.opacity = "0.6";
embedButton.style.cursor = "not-allowed";
const response: any = await new Promise((resolve, reject) => {
const response = await new Promise<
| {
data?: Array<{
id: number;
title: string;
slug: string;
lengthInMinutes?: number;
users?: Array<{ username?: string }>;
}>;
}
| Array<{
id: number;
title: string;
slug: string;
lengthInMinutes?: number;
users?: Array<{ username?: string }>;
}>
>((resolve, reject) => {
chrome.runtime.sendMessage({ action: "fetch-event-types" }, (result) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else if (result && result.error) {
} else if (result?.error) {
reject(new Error(result.error));
} else {
resolve(result);
@@ -2770,9 +2901,14 @@ export default defineContentScript({
});
});
const eventTypes = response?.data || (Array.isArray(response) ? response : []);
const eventTypes =
(response && "data" in response
? response.data
: Array.isArray(response)
? response
: []) || [];
const matchingEventType = eventTypes.find(
(et: any) => et.lengthInMinutes === freshParsedData.detectedDuration
(et) => et.lengthInMinutes === freshParsedData.detectedDuration
);
if (!matchingEventType) {
@@ -2808,9 +2944,7 @@ export default defineContentScript({
// Immediately remove the Google chip and action bar
try {
chipElement.remove();
if ((actionBar as any).__cleanup) {
(actionBar as any).__cleanup();
}
(actionBar as HTMLElement & { __cleanup?: () => void }).__cleanup?.();
actionBar.remove();
} catch (removeError) {
console.warn("Cal.com: Failed to remove chip/action bar:", removeError);
@@ -2882,11 +3016,9 @@ export default defineContentScript({
// Remove the action bar completely
try {
// Call cleanup function if it exists
if ((actionBar as any).__cleanup) {
(actionBar as any).__cleanup();
}
(actionBar as HTMLElement & { __cleanup?: () => void }).__cleanup?.();
actionBar.remove();
} catch (error) {
} catch (_error) {
// Silently ignore removal errors
}
});
@@ -2916,7 +3048,7 @@ export default defineContentScript({
actionBar.style.top = `${top}px`;
actionBar.style.left = `${left}px`;
} catch (error) {
} catch (_error) {
// Silently ignore positioning errors
}
};
@@ -2933,11 +3065,9 @@ export default defineContentScript({
if (barScheduleId === scheduleId) {
// This bar belongs to the current chip - remove it (we'll create a fresh one)
try {
if ((bar as any).__cleanup) {
(bar as any).__cleanup();
}
(bar as Element & { __cleanup?: () => void }).__cleanup?.();
bar.remove();
} catch (e) {
} catch (_e) {
// Ignore removal errors
}
} else {
@@ -2948,13 +3078,11 @@ export default defineContentScript({
);
if (!chipForBar) {
// Chip is gone, this is an orphaned action bar - remove it
if ((bar as any).__cleanup) {
(bar as any).__cleanup();
}
(bar as Element & { __cleanup?: () => void }).__cleanup?.();
bar.remove();
}
// Else: chip exists, keep this action bar (belongs to another chip)
} catch (e) {
} catch (_e) {
// Ignore errors when checking
}
}
@@ -2976,7 +3104,7 @@ export default defineContentScript({
// Chip removed, clean up
try {
actionBar.remove();
} catch (e) {
} catch (_e) {
// Ignore
}
}
@@ -2986,20 +3114,20 @@ export default defineContentScript({
window.addEventListener("resize", updatePosition);
// Store cleanup function
(actionBar as any).__cleanup = () => {
(actionBar as HTMLElement & { __cleanup?: () => void }).__cleanup = () => {
window.removeEventListener("scroll", updatePosition, true);
window.removeEventListener("resize", updatePosition);
};
} catch (error) {
} catch (_error) {
console.error("Cal.com: Failed to create or insert action bar");
// Clean up if something failed
try {
actionBar.remove();
} catch (e) {
} catch (_e) {
// Ignore cleanup errors
}
}
} catch (error) {
} catch (_error) {
// Catch-all to prevent breaking Gmail UI
console.error("Cal.com: Error handling Google chip");
return;
@@ -3009,7 +3137,22 @@ export default defineContentScript({
/**
* Show Cal.com suggestion menu for Google Calendar chip - CENTERED FULL-SCREEN MODAL
*/
async function showCalcomSuggestionMenu(chipElement: HTMLElement, parsedData: any) {
async function showCalcomSuggestionMenu(
chipElement: HTMLElement,
parsedData: {
detectedDuration: number;
slots: Array<{
isoDate: string;
date: string;
isoTimestamp: string;
time?: string;
startTime?: string;
endTime?: string;
}>;
timezone: string;
timezoneOffset: string;
}
) {
console.log(
"Cal.com: Opening menu for",
parsedData.detectedDuration,
@@ -3125,22 +3268,48 @@ export default defineContentScript({
try {
// Fetch event types (will use cache if available)
let eventTypes: any[] = [];
let eventTypes: Array<{
id: number;
title: string;
slug: string;
lengthInMinutes?: number;
description?: string;
users?: Array<{ username?: string }>;
}> = [];
// Check cache first
const now = Date.now();
const isCacheValid =
eventTypesCache && cacheTimestamp && now - cacheTimestamp < CACHE_DURATION;
if (isCacheValid) {
eventTypes = eventTypesCache!;
if (isCacheValid && eventTypesCache) {
eventTypes = eventTypesCache;
} else {
// Fetch from background script
const response: any = await new Promise((resolve, reject) => {
const response = await new Promise<
| {
data?: Array<{
id: number;
title: string;
slug: string;
lengthInMinutes?: number;
description?: string;
users?: Array<{ username?: string }>;
}>;
}
| Array<{
id: number;
title: string;
slug: string;
lengthInMinutes?: number;
description?: string;
users?: Array<{ username?: string }>;
}>
>((resolve, reject) => {
chrome.runtime.sendMessage({ action: "fetch-event-types" }, (response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else if (response && response.error) {
} else if (response?.error) {
reject(new Error(response.error));
} else {
resolve(response);
@@ -3148,7 +3317,7 @@ export default defineContentScript({
});
});
if (response && response.data) {
if (response && "data" in response && response.data) {
eventTypes = response.data;
} else if (Array.isArray(response)) {
eventTypes = response;
@@ -3168,7 +3337,7 @@ export default defineContentScript({
// Filter event types by matching duration
const matchingEventTypes = eventTypes.filter(
(et: any) => et.lengthInMinutes === parsedData.detectedDuration
(et) => et.lengthInMinutes === parsedData.detectedDuration
);
// If no matching event types, show create prompt
@@ -3322,7 +3491,7 @@ export default defineContentScript({
`;
// Create options
matchingEventTypes.forEach((et: any, index: number) => {
matchingEventTypes.forEach((et, index: number) => {
const option = document.createElement("div");
option.className = "dropdown-option";
option.style.cssText = `
@@ -3409,7 +3578,7 @@ export default defineContentScript({
const slotsContainer = document.createElement("div");
slotsContainer.style.cssText = "padding: 12px;";
parsedData.slots.forEach((slot: any, index: number) => {
parsedData.slots.forEach((slot, index: number) => {
const slotItem = document.createElement("div");
slotItem.style.cssText = `
padding: 14px;
@@ -3479,7 +3648,9 @@ export default defineContentScript({
console.log("Cal.com: Insert button clicked!");
e.preventDefault();
e.stopPropagation();
const slotIndex = parseInt(btn.getAttribute("data-slot-index")!);
const slotIndexAttr = btn.getAttribute("data-slot-index");
if (!slotIndexAttr) return;
const slotIndex = parseInt(slotIndexAttr, 10);
const slot = parsedData.slots[slotIndex];
console.log("Cal.com: Inserting link for slot", slotIndex, slot);
@@ -3518,9 +3689,7 @@ export default defineContentScript({
chipElement.remove();
if (actionBar) {
if ((actionBar as any).__cleanup) {
(actionBar as any).__cleanup();
}
(actionBar as Element & { __cleanup?: () => void }).__cleanup?.();
actionBar.remove();
}
} catch (removeError) {
@@ -3623,7 +3792,21 @@ export default defineContentScript({
* Parse Google Calendar scheduling chip
* Returns null if structure has changed or parsing fails (fail gracefully)
*/
function parseGoogleChip(chipElement: HTMLElement): any {
function parseGoogleChip(chipElement: HTMLElement): {
scheduleId: string;
timezone: string;
timezoneOffset: string;
slots: Array<{
date: string;
startTime: string;
endTime: string;
durationMinutes: number;
isoDate: string;
isoTimestamp: string;
googleUrl: string;
}>;
detectedDuration: number;
} | null {
try {
// Validate input
if (!chipElement || typeof chipElement.getAttribute !== "function") {
@@ -3642,7 +3825,7 @@ export default defineContentScript({
console.log(
"Cal.com: Valid chip detected - Schedule ID:",
scheduleId?.slice(0, 20) + "..."
`${scheduleId?.slice(0, 20)}...`
);
// Parse timezone (non-critical - fallback to UTC if structure changed)
@@ -3669,7 +3852,7 @@ export default defineContentScript({
tzMatch = paramsAttr.match(/\\"([^\\]+)\\"\]/);
}
if (tzMatch && tzMatch[1]) {
if (tzMatch?.[1]) {
timezone = tzMatch[1]; // e.g., "Asia/Kolkata"
console.log("Cal.com: ✅ Parsed IANA timezone from data attribute:", timezone);
} else {
@@ -3706,7 +3889,15 @@ export default defineContentScript({
return null;
}
const slots: any[] = [];
const slots: Array<{
date: string;
startTime: string;
endTime: string;
durationMinutes: number;
isoDate: string;
isoTimestamp: string;
googleUrl: string;
}> = [];
let detectedDuration = 60;
slotLinks.forEach((link) => {
@@ -3722,18 +3913,18 @@ export default defineContentScript({
if (!slotStartTime || !slotDurationMinutes) return;
const durationMinutes = parseInt(slotDurationMinutes, 10);
if (isNaN(durationMinutes) || durationMinutes <= 0) return;
if (Number.isNaN(durationMinutes) || durationMinutes <= 0) return;
detectedDuration = durationMinutes;
const startTimestamp = parseInt(slotStartTime, 10);
if (isNaN(startTimestamp)) return;
if (Number.isNaN(startTimestamp)) return;
const startDate = new Date(startTimestamp);
const endDate = new Date(startDate.getTime() + durationMinutes * 60 * 1000);
// Validate dates
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) return;
if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) return;
const dateOptions: Intl.DateTimeFormatOptions = {
weekday: "short",
+8 -6
View File
@@ -289,7 +289,7 @@ function isEventInPast(eventElement: Element): boolean {
// Short date format: "Dec 20, 2024"
/(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),?\s+(\d{4})/i,
// Numeric format: "12/20/2024" or "20/12/2024"
/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,
/(\d{1,2})[/-](\d{1,2})[/-](\d{4})/,
];
const months: Record<string, number> = {
@@ -374,7 +374,7 @@ function isEventInPast(eventElement: Element): boolean {
if (eventStartAttr) {
try {
const eventDate = new Date(eventStartAttr);
if (!isNaN(eventDate.getTime())) {
if (!Number.isNaN(eventDate.getTime())) {
return eventDate < now;
}
} catch {
@@ -443,7 +443,7 @@ function parseAttendees(eventPopup: Element): AttendeeInfo[] {
// Extract name from the SDqFWd span (Google Calendar's name display)
let name = email.split("@")[0];
const nameSpan = el.querySelector(".SDqFWd span");
if (nameSpan && nameSpan.textContent) {
if (nameSpan?.textContent) {
const nameText = nameSpan.textContent.trim();
// Only use if it's not just the email
if (nameText !== email && nameText.includes("@") === false) {
@@ -689,7 +689,9 @@ function showConfirmDialog(
*/
function showAlert(message: string, type: "error" | "success" | "info" = "info"): void {
// Remove existing alerts
document.querySelectorAll(".cal-noshow-alert").forEach((el) => el.remove());
for (const el of document.querySelectorAll(".cal-noshow-alert")) {
el.remove();
}
const alert = document.createElement("div");
alert.className = `cal-noshow-alert ${type}`;
@@ -1092,7 +1094,7 @@ async function injectNoShowButtons(eventPopup: Element): Promise<void> {
? nameContainer.querySelector(".SDqFWd span, .SDqFWd")
: attendee.element.querySelector(".SDqFWd span, .SDqFWd");
if (nameSpan && nameSpan.parentElement) {
if (nameSpan?.parentElement) {
// Insert button AFTER the name span (on the right side)
// Check if button already exists at this location
const parent = nameSpan.parentElement;
@@ -1162,7 +1164,7 @@ async function injectNoShowButtons(eventPopup: Element): Promise<void> {
const emailElement = eventPopup.querySelector(`[data-email="${attendee.email}"]`);
if (emailElement && emailElement !== eventPopup) {
const nameSpan = emailElement.querySelector(".SDqFWd span, .SDqFWd");
if (nameSpan && nameSpan.parentElement) {
if (nameSpan?.parentElement) {
nameSpan.parentElement.insertBefore(buttonContainer, nameSpan.nextSibling);
inserted = true;
} else if (emailElement.parentElement) {
+10 -4
View File
@@ -300,7 +300,9 @@ export function initLinkedInIntegration() {
setTimeout(() => {
const closeMenu = (evt: MouseEvent) => {
if (!menu.contains(evt.target as Node) && evt.target !== button) {
tooltipsToCleanup.forEach((tooltip) => tooltip.remove());
for (const tooltip of tooltipsToCleanup) {
tooltip.remove();
}
menu.remove();
document.removeEventListener("click", closeMenu);
}
@@ -438,7 +440,7 @@ export function initLinkedInIntegration() {
);
menu.appendChild(menuItem);
});
} catch (error) {
} catch (_error) {
menu.innerHTML = `
<div style="padding: 16px; text-align: center; color: #ea4335;">
Error displaying event types
@@ -674,7 +676,9 @@ export function initLinkedInIntegration() {
errorMessage.includes("authentication");
if (isAuthError) {
tooltipsToCleanup.forEach((tooltip) => tooltip.remove());
for (const tooltip of tooltipsToCleanup) {
tooltip.remove();
}
menu.remove();
openCalSidebar();
return;
@@ -714,7 +718,9 @@ export function initLinkedInIntegration() {
closeBtn.style.background = "#F3F4F6";
});
closeBtn.addEventListener("click", () => {
tooltipsToCleanup.forEach((tooltip) => tooltip.remove());
for (const tooltip of tooltipsToCleanup) {
tooltip.remove();
}
menu.remove();
});
}
+59 -66
View File
@@ -10,73 +10,66 @@
* ```
*/
// Bookings hooks
export {
useBookings,
useBookingByUid,
useCancelBooking,
useRescheduleBooking,
useConfirmBooking,
useDeclineBooking,
usePrefetchBookings,
useInvalidateBookings,
type BookingFilters,
type Booking,
} from "./useBookings";
// Event Types hooks
export {
useEventTypes,
useEventTypeById,
useCreateEventType,
useUpdateEventType,
useDeleteEventType,
useDuplicateEventType,
usePrefetchEventTypes,
useInvalidateEventTypes,
type EventType,
type CreateEventTypeInput,
} from "./useEventTypes";
// Schedules (Availability) hooks
export {
useSchedules,
useScheduleById,
useCreateSchedule,
useUpdateSchedule,
useSetScheduleAsDefault,
useDeleteSchedule,
useDuplicateSchedule,
usePrefetchSchedules,
useInvalidateSchedules,
type Schedule,
type CreateScheduleInput,
type UpdateScheduleInput,
} from "./useSchedules";
// User Profile hooks
export {
useUserProfile,
useUsername,
useUpdateUserProfile,
usePrefetchUserProfile,
useInvalidateUserProfile,
type UserProfile,
type UpdateUserProfileInput,
} from "./useUserProfile";
// UI State Management hooks
export {
useActiveBookingFilter,
type BookingFilter,
type BookingFilterOption,
} from "./useActiveBookingFilter";
export { useBookingActions } from "./useBookingActions";
export { useBookingActionModals } from "./useBookingActionModals";
// Re-export query keys for advanced use cases
export { queryKeys } from "../config/cache.config";
// Re-export query context utilities
export { useQueryContext } from "../contexts/QueryContext";
// UI State Management hooks
export {
type BookingFilter,
type BookingFilterOption,
useActiveBookingFilter,
} from "./useActiveBookingFilter";
export { useBookingActionModals } from "./useBookingActionModals";
export { useBookingActions } from "./useBookingActions";
// Bookings hooks
export {
type Booking,
type BookingFilters,
useBookingByUid,
useBookings,
useCancelBooking,
useConfirmBooking,
useDeclineBooking,
useInvalidateBookings,
usePrefetchBookings,
useRescheduleBooking,
} from "./useBookings";
// Event Types hooks
export {
type CreateEventTypeInput,
type EventType,
useCreateEventType,
useDeleteEventType,
useDuplicateEventType,
useEventTypeById,
useEventTypes,
useInvalidateEventTypes,
usePrefetchEventTypes,
useUpdateEventType,
} from "./useEventTypes";
// Schedules (Availability) hooks
export {
type CreateScheduleInput,
type Schedule,
type UpdateScheduleInput,
useCreateSchedule,
useDeleteSchedule,
useDuplicateSchedule,
useInvalidateSchedules,
usePrefetchSchedules,
useScheduleById,
useSchedules,
useSetScheduleAsDefault,
useUpdateSchedule,
} from "./useSchedules";
// User Profile hooks
export {
type UpdateUserProfileInput,
type UserProfile,
useInvalidateUserProfile,
usePrefetchUserProfile,
useUpdateUserProfile,
useUsername,
useUserProfile,
} from "./useUserProfile";
+1 -1
View File
@@ -1,4 +1,4 @@
import { useState, useMemo, useCallback } from "react";
import { useCallback, useMemo, useState } from "react";
import type { NativeSyntheticEvent } from "react-native";
import type { BookingFilters } from "./useBookings";
+6 -5
View File
@@ -4,17 +4,18 @@
* Centralized hook for managing all booking action modals and their API calls.
* This hook is designed to be used across iOS, Android, and extension platforms.
*/
import { useState, useCallback } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { useCallback, useState } from "react";
import { Alert, Linking } from "react-native";
import { CalComAPIService, type Booking } from "../services/calcom";
import { queryKeys } from "../config/cache.config";
import { type Booking, CalComAPIService } from "../services/calcom";
import type {
AddGuestInput,
BookingRecording,
ConferencingSession,
AddGuestInput,
} from "../services/types/bookings.types";
import { showErrorAlert } from "../utils/alerts";
import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "../config/cache.config";
interface UseBookingActionModalsReturn {
// Selected booking for actions
+8 -8
View File
@@ -1,8 +1,8 @@
import type { Booking } from "../services/calcom";
import { showErrorAlert } from "../utils/alerts";
import type { useRouter } from "expo-router";
import { useState } from "react";
import { Alert } from "react-native";
import type { Booking } from "../services/calcom";
import { showErrorAlert } from "../utils/alerts";
interface UseBookingActionsParams {
router: ReturnType<typeof useRouter>;
@@ -90,7 +90,7 @@ export const useBookingActions = ({
const currentDate = new Date(startTimeValue);
// Check if the date is valid
if (isNaN(currentDate.getTime())) {
if (Number.isNaN(currentDate.getTime())) {
showErrorAlert("Error", "Unable to reschedule: invalid booking date");
return;
}
@@ -126,7 +126,7 @@ export const useBookingActions = ({
const newDateTime = new Date(dateTimeStr);
// Validate the date
if (isNaN(newDateTime.getTime())) {
if (Number.isNaN(newDateTime.getTime())) {
showErrorAlert(
"Error",
"Invalid date or time format. Please use YYYY-MM-DD for date and HH:MM for time."
@@ -179,7 +179,7 @@ export const useBookingActions = ({
const newDateTime = new Date(dateTimeStr);
// Validate the date
if (isNaN(newDateTime.getTime())) {
if (Number.isNaN(newDateTime.getTime())) {
throw new Error(
"Invalid date or time format. Please use YYYY-MM-DD for date and HH:MM for time."
);
@@ -250,7 +250,7 @@ export const useBookingActions = ({
onSuccess: () => {
Alert.alert("Success", "Event cancelled successfully");
},
onError: (error) => {
onError: (_error) => {
console.error("Failed to cancel booking");
showErrorAlert("Error", "Failed to cancel event. Please try again.");
},
@@ -359,7 +359,7 @@ export const useBookingActions = ({
setRejectReason("");
Alert.alert("Success", "Booking rejected successfully");
},
onError: (error) => {
onError: (_error) => {
showErrorAlert("Error", "Failed to reject booking. Please try again.");
},
}
@@ -385,7 +385,7 @@ export const useBookingActions = ({
onSuccess: () => {
Alert.alert("Success", "Booking confirmed successfully");
},
onError: (error) => {
onError: (_error) => {
showErrorAlert("Error", "Failed to confirm booking. Please try again.");
},
}
+4 -3
View File
@@ -4,15 +4,16 @@
* This hook provides the booking actions state using the centralized gating utility.
* It computes which actions are visible and enabled for a given booking.
*/
import { useMemo } from "react";
import type { Booking } from "../services/calcom";
import type { EventType } from "../services/types/event-types.types";
import {
normalizeBooking,
getBookingActions,
type BookingActionsResult,
getBookingActions,
type NormalizedBooking,
normalizeBooking,
} from "../utils/booking-actions";
import { useMemo } from "react";
interface UseBookingActionsGatingParams {
booking: Booking | null;
+11 -7
View File
@@ -8,9 +8,10 @@
* - Optimistic updates for mutations
* - Cache invalidation on mutations
*/
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { CACHE_CONFIG, queryKeys } from "../config/cache.config";
import { CalComAPIService, Booking } from "../services/calcom";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { type Booking, CalComAPIService } from "../services/calcom";
/**
* Filter options for fetching bookings
@@ -73,7 +74,10 @@ export function useBookings(filters?: BookingFilters) {
export function useBookingByUid(uid: string | undefined) {
return useQuery({
queryKey: queryKeys.bookings.detail(uid || ""),
queryFn: () => CalComAPIService.getBookingByUid(uid!),
queryFn: () => {
if (!uid) throw new Error("uid is required");
return CalComAPIService.getBookingByUid(uid);
},
enabled: !!uid, // Only fetch when uid is provided
staleTime: CACHE_CONFIG.bookings.staleTime,
});
@@ -106,7 +110,7 @@ export function useCancelBooking() {
queryKey: queryKeys.bookings.detail(variables.uid),
});
},
onError: (error) => {
onError: (_error) => {
console.error("Failed to cancel booking");
},
});
@@ -150,7 +154,7 @@ export function useRescheduleBooking() {
queryKey: queryKeys.bookings.detail(variables.uid),
});
},
onError: (error) => {
onError: (_error) => {
console.error("Failed to reschedule booking");
},
});
@@ -182,7 +186,7 @@ export function useConfirmBooking() {
queryKey: queryKeys.bookings.detail(variables.uid),
});
},
onError: (error) => {
onError: (_error) => {
console.error("Failed to confirm booking");
},
});
@@ -215,7 +219,7 @@ export function useDeclineBooking() {
queryKey: queryKeys.bookings.detail(variables.uid),
});
},
onError: (error) => {
onError: (_error) => {
console.error("Failed to decline booking");
},
});
+7 -3
View File
@@ -8,9 +8,10 @@
* - Optimistic updates for mutations
* - Cache invalidation on create/update/delete
*/
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { CACHE_CONFIG, queryKeys } from "../config/cache.config";
import { CalComAPIService, EventType, CreateEventTypeInput } from "../services/calcom";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { CalComAPIService, type CreateEventTypeInput, type EventType } from "../services/calcom";
/**
* Hook to fetch all event types
@@ -57,7 +58,10 @@ export function useEventTypes() {
export function useEventTypeById(id: number | undefined) {
return useQuery({
queryKey: queryKeys.eventTypes.detail(id || 0),
queryFn: () => CalComAPIService.getEventTypeById(id!),
queryFn: () => {
if (!id) throw new Error("id is required");
return CalComAPIService.getEventTypeById(id);
},
enabled: !!id, // Only fetch when id is provided
staleTime: CACHE_CONFIG.eventTypes.staleTime,
});
+2 -2
View File
@@ -4,8 +4,8 @@
* This hook provides real-time network status for components.
* Used to disable mutations when offline.
*/
import NetInfo, { NetInfoState } from "@react-native-community/netinfo";
import { useState, useEffect } from "react";
import NetInfo, { type NetInfoState } from "@react-native-community/netinfo";
import { useEffect, useState } from "react";
interface NetworkStatus {
isOnline: boolean;
+12 -8
View File
@@ -8,9 +8,10 @@
* - Optimistic updates for mutations
* - Cache invalidation on create/update/delete
*/
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { CACHE_CONFIG, queryKeys } from "../config/cache.config";
import { CalComAPIService, Schedule } from "../services/calcom";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { CalComAPIService, type Schedule } from "../services/calcom";
/**
* Sort schedules: default first, then alphabetically by name
@@ -109,7 +110,10 @@ export function useSchedules() {
export function useScheduleById(id: number | undefined) {
return useQuery({
queryKey: queryKeys.schedules.detail(id || 0),
queryFn: () => CalComAPIService.getScheduleById(id!),
queryFn: () => {
if (!id) throw new Error("id is required");
return CalComAPIService.getScheduleById(id);
},
enabled: !!id, // Only fetch when id is provided
staleTime: CACHE_CONFIG.schedules.staleTime,
});
@@ -145,7 +149,7 @@ export function useCreateSchedule() {
// Optionally, add the new schedule to cache immediately
queryClient.setQueryData(queryKeys.schedules.detail(newSchedule.id), newSchedule);
},
onError: (error) => {
onError: (_error) => {
console.error("Failed to create schedule");
},
});
@@ -179,7 +183,7 @@ export function useUpdateSchedule() {
// Update the specific schedule in cache
queryClient.setQueryData(queryKeys.schedules.detail(variables.id), updatedSchedule);
},
onError: (error) => {
onError: (_error) => {
console.error("Failed to update schedule");
},
});
@@ -206,7 +210,7 @@ export function useSetScheduleAsDefault() {
// Invalidate all schedules to update the default flag
queryClient.invalidateQueries({ queryKey: queryKeys.schedules.all });
},
onError: (error) => {
onError: (_error) => {
console.error("Failed to set schedule as default");
},
});
@@ -246,7 +250,7 @@ export function useDeleteSchedule() {
return { previousSchedules };
},
onError: (error, _deletedId, context) => {
onError: (_error, _deletedId, context) => {
// Rollback on error
if (context?.previousSchedules) {
queryClient.setQueryData(queryKeys.schedules.lists(), context.previousSchedules);
@@ -281,7 +285,7 @@ export function useDuplicateSchedule() {
// Invalidate the list to include the duplicated schedule
queryClient.invalidateQueries({ queryKey: queryKeys.schedules.lists() });
},
onError: (error) => {
onError: (_error) => {
console.error("Failed to duplicate schedule");
},
});
+3 -2
View File
@@ -6,9 +6,10 @@
* - Automatic caching with configurable stale times
* - Profile update mutations with cache invalidation
*/
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { CACHE_CONFIG, queryKeys } from "../config/cache.config";
import { CalComAPIService, UserProfile } from "../services/calcom";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { CalComAPIService, type UserProfile } from "../services/calcom";
/**
* User profile update input type
+1 -1
View File
@@ -4,8 +4,8 @@
*/
export {
createQueryPersister,
clearQueryCache,
createQueryPersister,
getCacheMetadata,
storage,
} from "../utils/queryPersister";
+1 -1
View File
@@ -4,8 +4,8 @@
*/
export {
secureStorage,
generalStorage,
isChromeStorageAvailable,
type StorageAdapter,
secureStorage,
} from "../utils/storage";
+8 -6
View File
@@ -40,8 +40,11 @@
"ext:zip-edge-prod": "BUILD_FOR_STORE=true BROWSER_TARGET=edge wxt zip -b edge",
"ext:build-all": "npm run ext:build && npm run ext:build-firefox && npm run ext:build-safari && npm run ext:build-edge",
"ext:build-all-prod": "npm run ext:build-prod && npm run ext:build-firefox-prod && npm run ext:build-safari-prod && npm run ext:build-edge-prod",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\"",
"format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,css,md}\"",
"lint": "biome lint .",
"format": "biome format --write .",
"format:check": "biome format .",
"check": "biome check --write .",
"check:ci": "biome ci .",
"prepare": "cd .. && husky companion/.husky"
},
"dependencies": {
@@ -81,21 +84,20 @@
},
"private": true,
"devDependencies": {
"@biomejs/biome": "2.3.10",
"@types/chrome": "^0.1.31",
"@types/react": "19.2.3",
"@types/react-dom": "19.2.3",
"babel-preset-expo": "^54.0.7",
"husky": "^9.0.11",
"lint-staged": "^15.2.0",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.4.17",
"typescript": "^5.9.3",
"wxt": "^0.20.11"
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx,json,css,md}": [
"prettier --write"
"**/*.{js,jsx,ts,tsx,json}": [
"biome check --write --error-on-warnings --no-errors-on-unmatched"
]
}
}
+1408 -1370
View File
File diff suppressed because it is too large Load Diff
-7
View File
@@ -89,7 +89,6 @@ export interface OAuthConfig {
export class CalComOAuthService {
private config: OAuthConfig;
private codeVerifier: string | null = null;
private state: string | null = null;
constructor(config: OAuthConfig) {
this.config = config;
@@ -101,7 +100,6 @@ export class CalComOAuthService {
const state = this.generateRandomBase64Url();
this.codeVerifier = codeVerifier;
this.state = state;
return { codeVerifier, codeChallenge, state };
}
@@ -427,7 +425,6 @@ export class CalComOAuthService {
clearPKCEParams(): void {
this.codeVerifier = null;
this.state = null;
}
async syncTokensToExtension(tokens: OAuthTokens): Promise<void> {
@@ -618,10 +615,6 @@ function getBrowserSpecificOAuthConfig(): { clientId: string; redirectUri: strin
clientId: process.env.EXPO_PUBLIC_CALCOM_OAUTH_CLIENT_ID_EDGE || defaultClientId,
redirectUri: process.env.EXPO_PUBLIC_CALCOM_OAUTH_REDIRECT_URI_EDGE || defaultRedirectUri,
};
case "chrome":
case "brave":
case "unknown":
default:
// Chrome, Brave, and unknown browsers use the default configuration
return { clientId: defaultClientId, redirectUri: defaultRedirectUri };
@@ -225,6 +225,18 @@ export interface EventType {
disableCancelling?: boolean;
minimumRescheduleNotice?: number;
allowReschedulingPastBookings?: boolean;
// Additional properties from API responses
hideCalendarEventDetails?: boolean;
hideOrganizerEmail?: boolean;
allowReschedulingCancelledBookings?: boolean;
customReplyToEmail?: string;
color?: {
lightThemeHex?: string;
darkThemeHex?: string;
};
sendCalVideoTranscription?: boolean;
autoTranslate?: boolean;
}
export interface CreateEventTypeInput {
+3 -2
View File
@@ -1,7 +1,8 @@
// Re-export all types for easy importing
export * from "./event-types.types";
export * from "./bookings.types";
export * from "./event-types.types";
export * from "./private-links.types";
export * from "./schedules.types";
export * from "./users.types";
export * from "./webhooks.types";
export * from "./private-links.types";
+183 -175
View File
@@ -9,186 +9,194 @@ export interface WebSessionInfo {
userInfo?: UserProfile;
}
export class WebAuthService {
// Check if running on web
static isWeb(): boolean {
return Platform.OS === "web";
// Check if running on web
function isWeb(): boolean {
return Platform.OS === "web";
}
// Get cookies from the browser
function getCookies(): { [key: string]: string } {
if (!isWeb()) return {};
const cookies: { [key: string]: string } = {};
if (typeof document !== "undefined") {
document.cookie.split(";").forEach((cookie) => {
const [name, value] = cookie.trim().split("=");
if (name && value) {
cookies[name] = decodeURIComponent(value);
}
});
}
return cookies;
}
// Check for Cal.com session cookies
async function checkCalComSession(): Promise<WebSessionInfo> {
if (!isWeb()) {
return { isLoggedIn: false };
}
// Get cookies from the browser
static getCookies(): { [key: string]: string } {
if (!this.isWeb()) return {};
try {
// Check for common Cal.com session cookies
const cookies = getCookies();
const cookies: { [key: string]: string } = {};
if (typeof document !== "undefined") {
document.cookie.split(";").forEach((cookie) => {
const [name, value] = cookie.trim().split("=");
if (name && value) {
cookies[name] = decodeURIComponent(value);
}
});
}
return cookies;
}
// Look for Cal.com authentication cookies
// Common patterns: next-auth.session-token, __Secure-next-auth.session-token, etc.
const sessionCookieNames = [
"next-auth.session-token",
"__Secure-next-auth.session-token",
"__Host-next-auth.session-token",
"next-auth.csrf-token",
"cal-session",
"session-token",
];
// Check for Cal.com session cookies
static async checkCalComSession(): Promise<WebSessionInfo> {
if (!this.isWeb()) {
return { isLoggedIn: false };
const foundSessionCookie = sessionCookieNames.find((name) => cookies[name]);
if (foundSessionCookie) {
// Try to validate the session with Cal.com API
const sessionInfo = await validateWebSession();
return sessionInfo;
}
try {
// Check for common Cal.com session cookies
const cookies = this.getCookies();
// Look for Cal.com authentication cookies
// Common patterns: next-auth.session-token, __Secure-next-auth.session-token, etc.
const sessionCookieNames = [
"next-auth.session-token",
"__Secure-next-auth.session-token",
"__Host-next-auth.session-token",
"next-auth.csrf-token",
"cal-session",
"session-token",
];
const foundSessionCookie = sessionCookieNames.find((name) => cookies[name]);
if (foundSessionCookie) {
// Try to validate the session with Cal.com API
const sessionInfo = await this.validateWebSession();
return sessionInfo;
}
return { isLoggedIn: false };
} catch (error) {
return { isLoggedIn: false };
}
}
// Validate web session by calling Cal.com API with cookies
static async validateWebSession(): Promise<WebSessionInfo> {
if (!this.isWeb()) {
return { isLoggedIn: false };
}
try {
// First try to call the NextAuth session endpoint
const sessionResponse = await fetch("https://app.cal.com/api/auth/session", {
method: "GET",
credentials: "include", // Include cookies
headers: {
"Content-Type": "application/json",
},
});
if (sessionResponse.ok) {
const sessionData = await sessionResponse.json();
if (sessionData && sessionData.user) {
// User is logged in via web session
return {
isLoggedIn: true,
userInfo: sessionData.user,
};
}
}
// Try the internal Cal.com me endpoint (this might work with cookies)
const meResponse = await fetch("https://app.cal.com/api/me", {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
});
if (meResponse.ok) {
const userData = await meResponse.json();
if (userData && (userData.id || userData.user || userData.data)) {
return {
isLoggedIn: true,
userInfo: userData.user || userData.data || userData,
};
}
}
// Try to check if user is logged in by attempting to access a protected page
const dashboardResponse = await fetch("https://app.cal.com/api/trpc/viewer.me", {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
});
if (dashboardResponse.ok) {
const dashboardData = await dashboardResponse.json();
if (dashboardData && dashboardData.result && dashboardData.result.data) {
return {
isLoggedIn: true,
userInfo: dashboardData.result.data,
};
}
}
return { isLoggedIn: false };
} catch (error) {
return { isLoggedIn: false };
}
}
// Try to get API tokens from web session
static async getTokensFromWebSession(): Promise<{ accessToken?: string; refreshToken?: string }> {
if (!this.isWeb()) {
return {};
}
try {
// Some Cal.com implementations might expose tokens via specific endpoints
// This is a fallback approach - in practice, web sessions might not expose tokens directly
// Check localStorage/sessionStorage for any stored tokens
if (typeof window !== "undefined") {
const localStorage = window.localStorage;
const sessionStorage = window.sessionStorage;
const possibleTokenKeys = [
"cal-access-token",
"cal-auth-token",
"next-auth.token",
"auth-token",
"access-token",
];
for (const key of possibleTokenKeys) {
const localToken = localStorage.getItem(key);
const sessionToken = sessionStorage.getItem(key);
if (localToken || sessionToken) {
return {
accessToken: localToken || sessionToken || undefined,
};
}
}
}
return {};
} catch (error) {
return {};
}
}
// Redirect to Cal.com web login instead of using WebView
static redirectToWebLogin(): void {
if (!this.isWeb()) return;
// For web, redirect directly to Cal.com login
const currentUrl = window.location.href;
const loginUrl = `https://app.cal.com/auth/signin?callbackUrl=${encodeURIComponent(currentUrl)}`;
window.location.href = loginUrl;
return { isLoggedIn: false };
} catch (_error) {
return { isLoggedIn: false };
}
}
// Validate web session by calling Cal.com API with cookies
async function validateWebSession(): Promise<WebSessionInfo> {
if (!isWeb()) {
return { isLoggedIn: false };
}
try {
// First try to call the NextAuth session endpoint
const sessionResponse = await fetch("https://app.cal.com/api/auth/session", {
method: "GET",
credentials: "include", // Include cookies
headers: {
"Content-Type": "application/json",
},
});
if (sessionResponse.ok) {
const sessionData = await sessionResponse.json();
if (sessionData?.user) {
// User is logged in via web session
return {
isLoggedIn: true,
userInfo: sessionData.user,
};
}
}
// Try the internal Cal.com me endpoint (this might work with cookies)
const meResponse = await fetch("https://app.cal.com/api/me", {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
});
if (meResponse.ok) {
const userData = await meResponse.json();
if (userData && (userData.id || userData.user || userData.data)) {
return {
isLoggedIn: true,
userInfo: userData.user || userData.data || userData,
};
}
}
// Try to check if user is logged in by attempting to access a protected page
const dashboardResponse = await fetch("https://app.cal.com/api/trpc/viewer.me", {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
});
if (dashboardResponse.ok) {
const dashboardData = await dashboardResponse.json();
if (dashboardData?.result?.data) {
return {
isLoggedIn: true,
userInfo: dashboardData.result.data,
};
}
}
return { isLoggedIn: false };
} catch (_error) {
return { isLoggedIn: false };
}
}
// Try to get API tokens from web session
async function getTokensFromWebSession(): Promise<{ accessToken?: string; refreshToken?: string }> {
if (!isWeb()) {
return {};
}
try {
// Some Cal.com implementations might expose tokens via specific endpoints
// This is a fallback approach - in practice, web sessions might not expose tokens directly
// Check localStorage/sessionStorage for any stored tokens
if (typeof window !== "undefined") {
const localStorage = window.localStorage;
const sessionStorage = window.sessionStorage;
const possibleTokenKeys = [
"cal-access-token",
"cal-auth-token",
"next-auth.token",
"auth-token",
"access-token",
];
for (const key of possibleTokenKeys) {
const localToken = localStorage.getItem(key);
const sessionToken = sessionStorage.getItem(key);
if (localToken || sessionToken) {
return {
accessToken: localToken || sessionToken || undefined,
};
}
}
}
return {};
} catch (_error) {
return {};
}
}
// Redirect to Cal.com web login instead of using WebView
function redirectToWebLogin(): void {
if (!isWeb()) return;
// For web, redirect directly to Cal.com login
const currentUrl = window.location.href;
const loginUrl = `https://app.cal.com/auth/signin?callbackUrl=${encodeURIComponent(currentUrl)}`;
window.location.href = loginUrl;
}
// Export as object to satisfy noStaticOnlyClass rule
export const WebAuthService = {
isWeb,
getCookies,
checkCalComSession,
validateWebSession,
getTokensFromWebSession,
redirectToWebLogin,
};
-1
View File
@@ -3,4 +3,3 @@
*/
export * from "./locations";
+4 -2
View File
@@ -130,7 +130,7 @@ export function normalizeBooking(booking: Booking): NormalizedBooking {
startTime: new Date(startTimeStr),
endTime: new Date(endTimeStr),
location: booking.location,
isRecorded: (booking as any).isRecorded,
isRecorded: (booking as { isRecorded?: boolean }).isRecorded,
rescheduled: booking.rescheduled,
fromReschedule: booking.fromReschedule,
recurringEventId: booking.recurringEventId,
@@ -138,7 +138,9 @@ export function normalizeBooking(booking: Booking): NormalizedBooking {
user: booking.user,
hosts: booking.hosts,
attendees: booking.attendees,
seatsReferences: (booking as any).seatsReferences,
seatsReferences: (
booking as { seatsReferences?: Array<{ referenceUid: string; attendee: { email: string } }> }
).seatsReferences,
payment: booking.payment,
};
}
+23 -17
View File
@@ -47,7 +47,7 @@ export const formatTime = (dateString: string): string => {
}
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
if (Number.isNaN(date.getTime())) {
console.warn("Invalid date string:", dateString);
return "";
}
@@ -74,7 +74,7 @@ export const formatDate = (dateString: string, isUpcoming: boolean): string => {
}
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
if (Number.isNaN(date.getTime())) {
console.warn("Invalid date string:", dateString);
return "";
}
@@ -114,7 +114,7 @@ export const formatMonthYear = (dateString: string): string => {
if (!dateString) return "";
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
if (Number.isNaN(date.getTime())) {
return "";
}
const now = new Date();
@@ -129,7 +129,7 @@ export const formatMonthYear = (dateString: string): string => {
month: "long",
year: "numeric",
});
} catch (error) {
} catch (_error) {
return "";
}
};
@@ -143,11 +143,11 @@ export const getMonthYearKey = (dateString: string): string => {
if (!dateString) return "";
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
if (Number.isNaN(date.getTime())) {
return "";
}
return `${date.getFullYear()}-${date.getMonth()}`;
} catch (error) {
} catch (_error) {
return "";
}
};
@@ -211,23 +211,29 @@ export const searchBookings = (bookings: Booking[], searchQuery: string): Bookin
// Search in booking title
booking.title?.toLowerCase().includes(searchLower) ||
// Search in booking description
booking.description?.toLowerCase().includes(searchLower) ||
booking.description
?.toLowerCase()
.includes(searchLower) ||
// Search in event type title
booking.eventType?.title?.toLowerCase().includes(searchLower) ||
booking.eventType?.title
?.toLowerCase()
.includes(searchLower) ||
// Search in attendee names
(booking.attendees &&
booking.attendees.some((attendee) => attendee.name?.toLowerCase().includes(searchLower))) ||
booking.attendees?.some((attendee) => attendee.name?.toLowerCase().includes(searchLower)) ||
// Search in attendee emails
(booking.attendees &&
booking.attendees.some((attendee) =>
attendee.email?.toLowerCase().includes(searchLower)
)) ||
booking.attendees?.some((attendee) => attendee.email?.toLowerCase().includes(searchLower)) ||
// Search in location
booking.location?.toLowerCase().includes(searchLower) ||
booking.location
?.toLowerCase()
.includes(searchLower) ||
// Search in user name
booking.user?.name?.toLowerCase().includes(searchLower) ||
booking.user?.name
?.toLowerCase()
.includes(searchLower) ||
// Search in user email
booking.user?.email?.toLowerCase().includes(searchLower)
booking.user?.email
?.toLowerCase()
.includes(searchLower)
);
};
+2 -1
View File
@@ -4,8 +4,9 @@
* Centralized utility for opening links in the in-app browser.
* Configured for session sharing with Safari/Chrome to maintain login state.
*/
import { showErrorAlert } from "./alerts";
import * as WebBrowser from "expo-web-browser";
import { showErrorAlert } from "./alerts";
/**
* Configuration options for in-app browser
+3 -3
View File
@@ -10,7 +10,7 @@
*/
export const parseBufferTime = (buffer: string): number => {
const match = buffer.match(/(\d+)/);
return match ? parseInt(match[1]) : 0;
return match ? parseInt(match[1], 10) : 0;
};
/**
@@ -20,7 +20,7 @@ export const parseBufferTime = (buffer: string): number => {
* @returns Total minutes
*/
export const parseMinimumNotice = (value: string, unit: string): number => {
const val = parseInt(value) || 0;
const val = parseInt(value, 10) || 0;
if (unit === "Hours") return val * 60;
if (unit === "Days") return val * 24 * 60;
return val; // Minutes
@@ -47,5 +47,5 @@ export const parseFrequencyUnit = (unit: string): string | null => {
*/
export const parseSlotInterval = (interval: string): number => {
const match = interval.match(/(\d+)/);
return match ? parseInt(match[1]) : 0;
return match ? parseInt(match[1], 10) : 0;
};
+1 -1
View File
@@ -14,7 +14,7 @@
* formatDuration(90) // "1h 30m"
*/
export const formatDuration = (minutes: number | string | undefined): string => {
const mins = typeof minutes === "string" ? parseInt(minutes) || 0 : minutes || 0;
const mins = typeof minutes === "string" ? parseInt(minutes, 10) || 0 : minutes || 0;
if (mins <= 0) return "0m";
if (mins < 60) return `${mins}m`;
const hours = Math.floor(mins / 60);
+1 -1
View File
@@ -1,4 +1,4 @@
import { EventType } from "../hooks";
import type { EventType } from "../hooks";
export const getEventDuration = (eventType: EventType): number => {
// Prefer lengthInMinutes (API field), fallback to length for backwards compatibility
+1 -1
View File
@@ -54,7 +54,7 @@ export function parseGoogleChip(chipElement: HTMLElement): ParsedGoogleChip | nu
tzMatch = paramsAttr.match(/\\"([^\\]+)\\"\]/);
}
if (tzMatch && tzMatch[1]) {
if (tzMatch?.[1]) {
timezone = tzMatch[1]; // e.g., "Asia/Kolkata"
}
}
+40 -52
View File
@@ -10,76 +10,64 @@
* ```
*/
// Formatting utilities
export { formatDuration, truncateTitle, formatAppIdToDisplayName } from "./formatters";
// Storage utilities
export {
secureStorage,
generalStorage,
isChromeStorageAvailable,
type StorageAdapter,
} from "./storage";
// Query persistence utilities
export { createQueryPersister, clearQueryCache, getCacheMetadata } from "./queryPersister";
// Alert utilities
export { showErrorAlert } from "./alerts";
// Browser utilities
export { openInAppBrowser } from "./browser";
// Network utilities
export { isOnline } from "./network";
// Slug utilities
export { slugify } from "./slugify";
// App icon utilities
export { getAppIconUrl } from "./getAppIconUrl";
// Avatar utilities
export { getAvatarUrl } from "./getAvatarUrl";
// Default location utilities
export {
type DefaultLocation,
DefaultLocationType,
defaultLocations,
getDefaultLocationIconUrl,
isDefaultLocation,
DefaultLocationType,
type DefaultLocation,
} from "./defaultLocations";
// Location helper utilities
export {
formatAppIdToDisplayName as formatAppId, // Alias for backward compatibility
generateLocationId,
mapApiLocationToItem,
mapItemToApiLocation,
getLocationDisplayName,
getLocationIconUrl,
createLocationItemFromOption,
locationRequiresInput,
getLocationInputType,
getLocationInputPlaceholder,
getLocationInputLabel,
validateLocationItem,
buildLocationOptions,
displayNameToLocationValue,
} from "./locationHelpers";
// Event type parser utilities
export {
parseBufferTime,
parseMinimumNotice,
parseFrequencyUnit,
parseMinimumNotice,
parseSlotInterval,
} from "./eventTypeParsers";
// Formatting utilities
export { formatAppIdToDisplayName, formatDuration, truncateTitle } from "./formatters";
// App icon utilities
export { getAppIconUrl } from "./getAppIconUrl";
// Avatar utilities
export { getAvatarUrl } from "./getAvatarUrl";
// Gmail Google Chip parser (for extension)
export {
parseGoogleChip,
type GoogleTimeSlot,
type ParsedGoogleChip,
parseGoogleChip,
} from "./gmailGoogleChipParser";
// Location helper utilities
export {
buildLocationOptions,
createLocationItemFromOption,
displayNameToLocationValue,
formatAppIdToDisplayName as formatAppId, // Alias for backward compatibility
generateLocationId,
getLocationDisplayName,
getLocationIconUrl,
getLocationInputLabel,
getLocationInputPlaceholder,
getLocationInputType,
locationRequiresInput,
mapApiLocationToItem,
mapItemToApiLocation,
validateLocationItem,
} from "./locationHelpers";
// Network utilities
export { isOnline } from "./network";
// Query persistence utilities
export { clearQueryCache, createQueryPersister, getCacheMetadata } from "./queryPersister";
// Slug utilities
export { slugify } from "./slugify";
// Storage utilities
export {
generalStorage,
isChromeStorageAvailable,
type StorageAdapter,
secureStorage,
} from "./storage";

Some files were not shown because too many files have changed in this diff Show More