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:
@@ -1,10 +0,0 @@
|
||||
module.exports = {
|
||||
semi: true,
|
||||
trailingComma: "es5",
|
||||
singleQuote: false,
|
||||
printWidth: 100,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
arrowParens: "always",
|
||||
plugins: ["prettier-plugin-tailwindcss"],
|
||||
};
|
||||
@@ -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("");
|
||||
|
||||
@@ -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("");
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,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
@@ -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
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
module.exports = function (api) {
|
||||
module.exports = (api) => {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: [["babel-preset-expo", { jsxImportSource: "nativewind" }], "nativewind/babel"],
|
||||
|
||||
@@ -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
@@ -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,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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
import React from "react";
|
||||
import Svg, { Path } from "react-native-svg";
|
||||
|
||||
interface CalComLogoProps {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}>
|
||||
"{booking.description}"
|
||||
"{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}>
|
||||
"{booking.description}"
|
||||
"{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[];
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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,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";
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
export {
|
||||
createQueryPersister,
|
||||
clearQueryCache,
|
||||
createQueryPersister,
|
||||
getCacheMetadata,
|
||||
storage,
|
||||
} from "../utils/queryPersister";
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
export {
|
||||
secureStorage,
|
||||
generalStorage,
|
||||
isChromeStorageAvailable,
|
||||
type StorageAdapter,
|
||||
secureStorage,
|
||||
} from "../utils/storage";
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
@@ -3,4 +3,3 @@
|
||||
*/
|
||||
|
||||
export * from "./locations";
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user