feat(companion): TypeScript best practices improvements (#26206)
* feat(companion): TypeScript best practices improvements - Add separate tsconfig.extension.json for extension code typechecking - Add typecheck scripts: typecheck, typecheck:extension, typecheck:all - Create ambient declarations for browser extension APIs (browser.d.ts) - Create type definitions for attendee and google-calendar types - Replace all @ts-ignore with @ts-expect-error or remove where ambient declarations cover them - Replace any types with proper types in AuthContext, webAuth, buildPartialUpdatePayload - Fix implicit any errors in extension content.ts - Update deepEqual function to use unknown instead of any for better type safety All typechecks now pass for both app and extension code. * feat(companion): enable full TypeScript strict mode (#26208) * feat(companion): enable strictNullChecks and strictFunctionTypes Phase 1 of strict mode enablement for the companion app. Changes: - Enable strictNullChecks and strictFunctionTypes in tsconfig.json - Fix null check for scheduleDetails in fetchScheduleDetails - Add explicit type annotations to limits arrays in event-type-detail.tsx - Filter conferencing options with null appId before passing to buildLocationOptions - Update LimitsTab interface to use union type for field parameter - Add optional chaining for onEdit, onDuplicate, onDelete in EventTypeListItem.ios.tsx - Add explicit type annotations to options arrays in AvailabilityListScreen.tsx - Add explicit type annotations to dateOptions and timeOptions in RescheduleScreen.tsx - Fix type guard return types in calcom.ts to explicitly return boolean All typechecks pass for both app and extension code. * feat(companion): enable full strict mode and fix any types (Phase 2 + Step 7) Phase 2: Enable full strict mode - Replace strictNullChecks + strictFunctionTypes with strict: true - Fix implicit any errors in Alert.prompt callbacks (BookingDetailScreen, useBookingActions) Step 7: Fix metadata any types in type definition files - Update UserProfile.metadata to Record<string, unknown> - Update CreateEventTypeInput.metadata to Record<string, unknown> - Update Booking.responses to Record<string, unknown> All typechecks pass for both app and extension code with full strict mode enabled. * use JSON.stringify() * fix restores the recursive key comparison with sorted keys * prevents false positives when objects have different keys * feat(companion): add tree shaking optimization scripts (#26212) * feat(companion): add tree shaking optimization scripts Add npm scripts to enable Expo's experimental tree shaking for production builds: - export: Basic expo export command - export:ios / export:android: Platform-specific exports - export:optimized: Export with tree shaking enabled (all platforms) - export:optimized:ios / export:optimized:android: Platform-specific with tree shaking Bundle size improvement measured: - Baseline: 5.48 MB (1804 modules) - With tree shaking: 5.25 MB (1831 modules) - Reduction: ~230 KB (4.2% smaller) Also documented tree shaking configuration options in .env.example for: - npm scripts (recommended) - Manual env var setting - EAS build profile configuration * revert .env.example file * revert .env.example file
This commit is contained in:
@@ -222,7 +222,11 @@ export default function EventTypeDetail() {
|
||||
];
|
||||
|
||||
const getLocationOptionsForDropdown = (): LocationOptionGroup[] => {
|
||||
return buildLocationOptions(conferencingOptions);
|
||||
// Filter out conferencing options with null appId
|
||||
const validOptions = conferencingOptions.filter(
|
||||
(opt): opt is ConferencingOption & { appId: string } => opt.appId !== null
|
||||
);
|
||||
return buildLocationOptions(validOptions);
|
||||
};
|
||||
|
||||
const handleAddLocation = (location: LocationItem) => {
|
||||
@@ -333,7 +337,7 @@ export default function EventTypeDetail() {
|
||||
setScheduleDetailsLoading(true);
|
||||
const scheduleDetails = await CalComAPIService.getScheduleById(scheduleId);
|
||||
setSelectedScheduleDetails(scheduleDetails);
|
||||
if (scheduleDetails.timeZone) {
|
||||
if (scheduleDetails?.timeZone) {
|
||||
setSelectedTimezone(scheduleDetails.timeZone);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -421,7 +425,7 @@ export default function EventTypeDetail() {
|
||||
// Load booking frequency limits
|
||||
if (eventType.bookingLimitsCount && !("disabled" in eventType.bookingLimitsCount)) {
|
||||
setLimitBookingFrequency(true);
|
||||
const limits = [];
|
||||
const limits: Array<{ id: number; value: string; unit: string }> = [];
|
||||
let idCounter = 1;
|
||||
if (eventType.bookingLimitsCount.day) {
|
||||
limits.push({
|
||||
@@ -459,7 +463,7 @@ export default function EventTypeDetail() {
|
||||
// Load duration limits
|
||||
if (eventType.bookingLimitsDuration && !("disabled" in eventType.bookingLimitsDuration)) {
|
||||
setLimitTotalDuration(true);
|
||||
const limits = [];
|
||||
const limits: Array<{ id: number; value: string; unit: string }> = [];
|
||||
let idCounter = 1;
|
||||
if (eventType.bookingLimitsDuration.day) {
|
||||
limits.push({
|
||||
|
||||
@@ -36,7 +36,7 @@ interface LimitsTabProps {
|
||||
toggleBookingFrequency: (value: boolean) => void;
|
||||
frequencyAnimationValue: Animated.Value;
|
||||
frequencyLimits: FrequencyLimit[];
|
||||
updateFrequencyLimit: (id: number, field: string, value: string) => void;
|
||||
updateFrequencyLimit: (id: number, field: "value" | "unit", value: string) => void;
|
||||
setShowFrequencyUnitDropdown: (id: number) => void;
|
||||
removeFrequencyLimit: (id: number) => void;
|
||||
addFrequencyLimit: () => void;
|
||||
@@ -50,7 +50,7 @@ interface LimitsTabProps {
|
||||
toggleTotalDuration: (value: boolean) => void;
|
||||
durationAnimationValue: Animated.Value;
|
||||
durationLimits: DurationLimit[];
|
||||
updateDurationLimit: (id: number, field: string, value: string) => void;
|
||||
updateDurationLimit: (id: number, field: "value" | "unit", value: string) => void;
|
||||
setShowDurationUnitDropdown: (id: number) => void;
|
||||
removeDurationLimit: (id: number) => void;
|
||||
addDurationLimit: () => void;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { EventType } from "../../../services/calcom";
|
||||
import type { LocationItem } from "../../../types/locations";
|
||||
import { mapItemToApiLocation } from "../../../utils/locationHelpers";
|
||||
import {
|
||||
parseBufferTime,
|
||||
parseMinimumNotice,
|
||||
parseFrequencyUnit,
|
||||
parseSlotInterval,
|
||||
} from "../../../utils/eventTypeParsers";
|
||||
import { mapItemToApiLocation } from "../../../utils/locationHelpers";
|
||||
|
||||
interface FrequencyLimit {
|
||||
id: number;
|
||||
@@ -105,7 +105,7 @@ function hasMultipleDurationsChanged(
|
||||
const currentDurations = selectedDurations.map(parseDurationString).sort((a, b) => a - b);
|
||||
const originalDurations = [...originalOptions].sort((a: number, b: number) => a - b);
|
||||
|
||||
if (!deepEqual(currentDurations, originalDurations)) return true;
|
||||
if (!areEqual(currentDurations, originalDurations)) return true;
|
||||
|
||||
// Also check if the default (main) duration changed
|
||||
const currentDefault = parseDurationString(defaultDuration || mainDuration);
|
||||
@@ -114,24 +114,22 @@ function hasMultipleDurationsChanged(
|
||||
return currentDefault !== originalDefault;
|
||||
}
|
||||
|
||||
function deepEqual(a: any, b: any): boolean {
|
||||
function areEqual(a: unknown, b: unknown): boolean {
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return a == b;
|
||||
if (typeof a !== typeof b) return false;
|
||||
if (a == null || b == null) return false;
|
||||
if (typeof a !== "object" || typeof b !== "object") return false;
|
||||
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) return false;
|
||||
return a.every((item, index) => deepEqual(item, b[index]));
|
||||
return a.every((item, index) => areEqual(item, b[index]));
|
||||
}
|
||||
|
||||
if (typeof a === "object" && typeof b === "object") {
|
||||
const keysA = Object.keys(a);
|
||||
const keysB = Object.keys(b);
|
||||
if (keysA.length !== keysB.length) return false;
|
||||
return keysA.every((key) => deepEqual(a[key], b[key]));
|
||||
}
|
||||
|
||||
return false;
|
||||
const objA = a as Record<string, unknown>;
|
||||
const objB = b as Record<string, unknown>;
|
||||
const keysA = Object.keys(objA).sort();
|
||||
const keysB = Object.keys(objB).sort();
|
||||
if (keysA.length !== keysB.length) return false;
|
||||
return keysA.every((key, index) => key === keysB[index] && areEqual(objA[key], objB[key]));
|
||||
}
|
||||
|
||||
function normalizeLocation(loc: any): any {
|
||||
@@ -172,7 +170,7 @@ function haveLocationsChanged(
|
||||
currentMapped.sort(sortByType);
|
||||
originalMapped.sort(sortByType);
|
||||
|
||||
return !deepEqual(currentMapped, originalMapped);
|
||||
return !areEqual(currentMapped, originalMapped);
|
||||
}
|
||||
|
||||
function hasBookingLimitsCountChanged(
|
||||
@@ -207,7 +205,7 @@ function hasBookingLimitsCountChanged(
|
||||
});
|
||||
}
|
||||
|
||||
return !deepEqual(currentLimits, originalLimits);
|
||||
return !areEqual(currentLimits, originalLimits);
|
||||
}
|
||||
|
||||
function hasBookingLimitsDurationChanged(
|
||||
@@ -242,7 +240,7 @@ function hasBookingLimitsDurationChanged(
|
||||
});
|
||||
}
|
||||
|
||||
return !deepEqual(currentLimits, originalLimits);
|
||||
return !areEqual(currentLimits, originalLimits);
|
||||
}
|
||||
|
||||
function hasBookingWindowChanged(
|
||||
@@ -368,7 +366,7 @@ function hasBookerLayoutsChanged(
|
||||
const currentNormalized = selectedLayouts.map(mapLayoutToApi).sort();
|
||||
const originalNormalized = originalEnabled.map((l: string) => mapLayoutToApi(l)).sort();
|
||||
|
||||
if (!deepEqual(currentNormalized, originalNormalized)) return true;
|
||||
if (!areEqual(currentNormalized, originalNormalized)) return true;
|
||||
return mapLayoutToApi(defaultLayout) !== mapLayoutToApi(originalDefault || "");
|
||||
}
|
||||
|
||||
|
||||
@@ -49,19 +49,19 @@ export const EventTypeListItem = ({
|
||||
{
|
||||
label: "Edit",
|
||||
icon: "pencil",
|
||||
onPress: () => onEdit(item),
|
||||
onPress: () => onEdit?.(item),
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
label: "Duplicate",
|
||||
icon: "square.on.square",
|
||||
onPress: () => onDuplicate(item),
|
||||
onPress: () => onDuplicate?.(item),
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
label: "Delete",
|
||||
icon: "trash",
|
||||
onPress: () => onDelete(item),
|
||||
onPress: () => onDelete?.(item),
|
||||
role: "destructive",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -97,7 +97,11 @@ export function AvailabilityListScreen({
|
||||
const handleScheduleLongPress = (schedule: Schedule) => {
|
||||
if (Platform.OS !== "ios") {
|
||||
// Fallback for non-iOS platforms (Android Alert supports max 3 buttons)
|
||||
const options = [];
|
||||
const options: Array<{
|
||||
text: string;
|
||||
onPress: () => void;
|
||||
style?: "destructive" | "cancel" | "default";
|
||||
}> = [];
|
||||
if (!schedule.isDefault) {
|
||||
options.push({ text: "Set as default", onPress: () => handleSetAsDefault(schedule) });
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ export function BookingDetailScreen({ uid, onActionsReady }: BookingDetailScreen
|
||||
{
|
||||
text: "Cancel Booking",
|
||||
style: "destructive",
|
||||
onPress: (reason) => {
|
||||
onPress: (reason?: string) => {
|
||||
performCancelBooking(reason?.trim() || "Cancelled by host");
|
||||
},
|
||||
},
|
||||
|
||||
@@ -122,7 +122,7 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
|
||||
// Generate date options for picker (next 90 days)
|
||||
const dateOptions = React.useMemo(() => {
|
||||
const options = [];
|
||||
const options: Array<{ label: string; value: Date }> = [];
|
||||
const today = new Date();
|
||||
for (let i = 0; i < 90; i++) {
|
||||
const date = new Date(today);
|
||||
@@ -141,7 +141,7 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
|
||||
// Generate time options (every 15 minutes)
|
||||
const timeOptions = React.useMemo(() => {
|
||||
const options = [];
|
||||
const options: Array<{ label: string; value: { hour: number; minute: number } }> = [];
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
for (let minute = 0; minute < 60; minute += 15) {
|
||||
const time = `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
|
||||
|
||||
@@ -4,17 +4,29 @@ import {
|
||||
OAuthTokens,
|
||||
CalComOAuthService,
|
||||
} from "../services/oauthService";
|
||||
import type { UserProfile } from "../services/types/users.types";
|
||||
import { WebAuthService } from "../services/webAuth";
|
||||
import { secureStorage } from "../utils/storage";
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* Simplified user info stored in auth context
|
||||
* Contains only the essential fields needed for the app
|
||||
*/
|
||||
interface AuthUserInfo {
|
||||
id: number;
|
||||
email: string;
|
||||
name: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
isAuthenticated: boolean;
|
||||
accessToken: string | null;
|
||||
refreshToken: string | null;
|
||||
userInfo: any;
|
||||
userInfo: AuthUserInfo | null;
|
||||
isWebSession: boolean;
|
||||
loginFromWebSession: (userInfo: any) => Promise<void>;
|
||||
loginFromWebSession: (userInfo: UserProfile) => Promise<void>;
|
||||
loginWithOAuth: () => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
loading: boolean;
|
||||
@@ -44,7 +56,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [refreshToken, setRefreshToken] = useState<string | null>(null);
|
||||
const [userInfo, setUserInfo] = useState<any>(null);
|
||||
const [userInfo, setUserInfo] = useState<AuthUserInfo | null>(null);
|
||||
const [isWebSession, setIsWebSession] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [oauthService] = useState(() => {
|
||||
@@ -237,9 +249,14 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const loginFromWebSession = async (sessionUserInfo: any) => {
|
||||
const loginFromWebSession = async (sessionUserInfo: UserProfile) => {
|
||||
try {
|
||||
setUserInfo(sessionUserInfo);
|
||||
setUserInfo({
|
||||
id: sessionUserInfo.id,
|
||||
email: sessionUserInfo.email,
|
||||
name: sessionUserInfo.name,
|
||||
username: sessionUserInfo.username,
|
||||
});
|
||||
setIsAuthenticated(true);
|
||||
setIsWebSession(true);
|
||||
await storage.set(AUTH_TYPE_KEY, "web_session");
|
||||
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Ambient declarations for browser extension APIs
|
||||
* These provide proper typing for Firefox/Safari browser namespace
|
||||
* and Brave-specific navigator properties
|
||||
*/
|
||||
|
||||
// Firefox/Safari use 'browser' namespace instead of 'chrome'
|
||||
// This is a WebExtension API that mirrors chrome.* APIs
|
||||
declare const browser: typeof chrome | undefined;
|
||||
|
||||
// Brave adds isBrave() to navigator
|
||||
interface Navigator {
|
||||
brave?: {
|
||||
isBrave: () => Promise<boolean>;
|
||||
};
|
||||
}
|
||||
|
||||
// WXT global function for defining background scripts
|
||||
declare function defineBackground(fn: () => void): void;
|
||||
@@ -35,7 +35,6 @@ function detectBrowser(): BrowserType {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
|
||||
// Check for Brave
|
||||
// @ts-ignore - Brave adds this to navigator
|
||||
if (navigator.brave && typeof navigator.brave.isBrave === "function") {
|
||||
return BrowserType.Brave;
|
||||
}
|
||||
@@ -95,9 +94,7 @@ function getBrowserDisplayName(): string {
|
||||
|
||||
// Get the appropriate browser API namespace
|
||||
function getBrowserAPI(): typeof chrome {
|
||||
// @ts-ignore - Firefox/Safari use browser namespace
|
||||
if (typeof browser !== "undefined" && browser.runtime) {
|
||||
// @ts-ignore
|
||||
if (typeof browser !== "undefined" && browser?.runtime) {
|
||||
return browser;
|
||||
}
|
||||
return chrome;
|
||||
@@ -130,8 +127,7 @@ function getTabsAPI(): typeof chrome.tabs | null {
|
||||
// Get action API with cross-browser support
|
||||
function getActionAPI(): typeof chrome.action | null {
|
||||
const api = getBrowserAPI();
|
||||
// @ts-ignore - Some browsers use browserAction instead of action
|
||||
return api?.action || api?.browserAction || null;
|
||||
return api?.action || null;
|
||||
}
|
||||
|
||||
// Check if the URL is a restricted page where content scripts can't run
|
||||
@@ -168,7 +164,6 @@ function openAppPage(): void {
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore - WXT provides this globally
|
||||
export default defineBackground(() => {
|
||||
const browserType = detectBrowser();
|
||||
const browserName = getBrowserDisplayName();
|
||||
@@ -377,8 +372,10 @@ async function handleExtensionOAuth(authUrl: string): Promise<string> {
|
||||
// Firefox and Safari use Promise-based API
|
||||
if (browserType === BrowserType.Firefox || browserType === BrowserType.Safari) {
|
||||
try {
|
||||
// @ts-ignore - Firefox/Safari return Promises
|
||||
const result = identityAPI.launchWebAuthFlow({ url: authUrl, interactive: true });
|
||||
const result = identityAPI.launchWebAuthFlow({
|
||||
url: authUrl,
|
||||
interactive: true,
|
||||
}) as Promise<string | undefined> | void;
|
||||
|
||||
if (result && typeof result.then === "function") {
|
||||
result
|
||||
|
||||
@@ -726,7 +726,7 @@ export default defineContentScript({
|
||||
}, 0);
|
||||
});
|
||||
|
||||
function createTooltip(text, buttonElement) {
|
||||
function createTooltip(text: string, buttonElement: HTMLElement): HTMLElement {
|
||||
const tooltip = document.createElement("div");
|
||||
tooltip.className = "cal-tooltip";
|
||||
tooltip.style.cssText = `
|
||||
@@ -790,7 +790,10 @@ export default defineContentScript({
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEventTypes(menu, tooltipsToCleanup) {
|
||||
async function fetchEventTypes(
|
||||
menu: HTMLElement,
|
||||
tooltipsToCleanup: HTMLElement[]
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Check cache first
|
||||
const now = Date.now();
|
||||
@@ -1213,7 +1216,10 @@ export default defineContentScript({
|
||||
}
|
||||
}
|
||||
|
||||
function insertEventTypeLink(eventType) {
|
||||
function insertEventTypeLink(eventType: {
|
||||
slug: string;
|
||||
users?: Array<{ username?: string }>;
|
||||
}): void {
|
||||
// Construct the Cal.com booking link
|
||||
const bookingUrl = `https://cal.com/${eventType.users?.[0]?.username || "user"}/${
|
||||
eventType.slug
|
||||
@@ -1237,7 +1243,10 @@ export default defineContentScript({
|
||||
}
|
||||
}
|
||||
|
||||
function copyEventTypeLink(eventType) {
|
||||
function copyEventTypeLink(eventType: {
|
||||
slug: string;
|
||||
users?: Array<{ username?: string }>;
|
||||
}): void {
|
||||
// Construct the Cal.com booking link
|
||||
const bookingUrl = `https://cal.com/${eventType.users?.[0]?.username || "user"}/${
|
||||
eventType.slug
|
||||
@@ -1261,7 +1270,7 @@ export default defineContentScript({
|
||||
}
|
||||
}
|
||||
|
||||
function insertTextAtCursor(text) {
|
||||
function insertTextAtCursor(text: string): boolean {
|
||||
// Find the active compose field
|
||||
// Gmail uses contenteditable divs for the compose body
|
||||
const composeBody =
|
||||
@@ -1314,7 +1323,7 @@ export default defineContentScript({
|
||||
return true;
|
||||
}
|
||||
|
||||
function showNotification(message, type) {
|
||||
function showNotification(message: string, type: "success" | "error"): void {
|
||||
const notification = document.createElement("div");
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Types for Google Calendar integration in the extension
|
||||
* These types are used for parsing and handling Google Calendar data
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a time slot parsed from Google Calendar
|
||||
*/
|
||||
export interface TimeSlot {
|
||||
date: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsed data from Google Calendar chip elements
|
||||
*/
|
||||
export interface ParsedGoogleCalendarData {
|
||||
title?: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
detectedDuration?: number;
|
||||
attendees?: string[];
|
||||
location?: string;
|
||||
slots: TimeSlot[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic event type information used in the extension
|
||||
*/
|
||||
export interface EventTypeBasic {
|
||||
id: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
lengthInMinutes: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from fetching event types
|
||||
*/
|
||||
export interface EventTypesResponse {
|
||||
data?: EventTypeBasic[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token request for OAuth exchange
|
||||
*/
|
||||
export interface TokenExchangeRequest {
|
||||
grant_type: string;
|
||||
client_id: string;
|
||||
code?: string;
|
||||
redirect_uri?: string;
|
||||
code_verifier?: string;
|
||||
refresh_token?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slots grouped by date for display
|
||||
*/
|
||||
export interface SlotsByDate {
|
||||
[date: string]: TimeSlot[];
|
||||
}
|
||||
@@ -242,7 +242,7 @@ export const useBookingActions = ({
|
||||
{
|
||||
text: "Cancel Event",
|
||||
style: "destructive",
|
||||
onPress: (reason) => {
|
||||
onPress: (reason?: string) => {
|
||||
const cancellationReason = reason?.trim() || "Event cancelled by host";
|
||||
cancelMutation(
|
||||
{ uid: booking.uid, reason: cancellationReason },
|
||||
|
||||
@@ -9,6 +9,14 @@
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"typecheck:extension": "tsc -p tsconfig.extension.json --noEmit",
|
||||
"typecheck:all": "npm run typecheck && npm run typecheck:extension",
|
||||
"export": "expo export",
|
||||
"export:ios": "expo export --platform ios",
|
||||
"export:android": "expo export --platform android",
|
||||
"export:optimized": "EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1 EXPO_UNSTABLE_TREE_SHAKING=1 expo export",
|
||||
"export:optimized:ios": "EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1 EXPO_UNSTABLE_TREE_SHAKING=1 expo export --platform ios",
|
||||
"export:optimized:android": "EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1 EXPO_UNSTABLE_TREE_SHAKING=1 expo export --platform android",
|
||||
"ext": "wxt",
|
||||
"ext:build": "wxt build",
|
||||
"ext:build-prod": "BUILD_FOR_STORE=true wxt build",
|
||||
|
||||
@@ -803,8 +803,8 @@ export class CalComAPIService {
|
||||
} else {
|
||||
const keys = Object.keys(response.data);
|
||||
if (keys.length > 0) {
|
||||
eventTypesArray = Object.values(response.data).filter(
|
||||
(item): item is EventType => item && typeof item === "object" && "id" in item
|
||||
eventTypesArray = Object.values(response.data).filter((item): item is EventType =>
|
||||
Boolean(item && typeof item === "object" && "id" in item)
|
||||
) as EventType[];
|
||||
}
|
||||
}
|
||||
@@ -900,9 +900,8 @@ export class CalComAPIService {
|
||||
// Convert object values to array as last resort
|
||||
const keys = Object.keys(response.data);
|
||||
if (keys.length > 0) {
|
||||
bookingsArray = Object.values(response.data).filter(
|
||||
(item): item is Booking =>
|
||||
item && typeof item === "object" && ("id" in item || "uid" in item)
|
||||
bookingsArray = Object.values(response.data).filter((item): item is Booking =>
|
||||
Boolean(item && typeof item === "object" && ("id" in item || "uid" in item))
|
||||
) as Booking[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,9 +59,10 @@ async function getExtensionSessionToken(): Promise<string | null> {
|
||||
clearTimeout(timeoutId);
|
||||
window.removeEventListener("message", messageHandler);
|
||||
|
||||
extensionSessionToken = event.data.sessionToken || "";
|
||||
const token = event.data.sessionToken || "";
|
||||
extensionSessionToken = token;
|
||||
sessionTokenPromise = null;
|
||||
resolve(extensionSessionToken);
|
||||
resolve(token);
|
||||
};
|
||||
|
||||
window.addEventListener("message", messageHandler);
|
||||
@@ -253,10 +254,8 @@ export class CalComOAuthService {
|
||||
}
|
||||
|
||||
// Try Firefox/Safari browser.identity API (Promise-based)
|
||||
// @ts-ignore - Firefox/Safari use browser namespace
|
||||
if (typeof browser !== "undefined" && browser.identity) {
|
||||
if (typeof browser !== "undefined" && browser?.identity) {
|
||||
try {
|
||||
// @ts-ignore - Firefox/Safari browser.identity returns Promise
|
||||
browser.identity
|
||||
.launchWebAuthFlow({ url: authUrl, interactive: true })
|
||||
.then((responseUrl: string | undefined) => {
|
||||
@@ -553,7 +552,6 @@ function detectBrowserType(): BrowserType {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
|
||||
// Check for Brave first (it identifies as Chrome but has Brave-specific properties)
|
||||
// @ts-ignore - Brave adds this to navigator
|
||||
if (navigator.brave && typeof navigator.brave.isBrave === "function") {
|
||||
return "brave";
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export interface Booking {
|
||||
location?: string;
|
||||
cancellationReason?: string;
|
||||
rejectionReason?: string;
|
||||
responses?: Record<string, any>;
|
||||
responses?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface GetBookingsResponse {
|
||||
|
||||
@@ -285,7 +285,7 @@ export interface CreateEventTypeInput {
|
||||
lockTimeZoneToggleOnBookingPage?: boolean;
|
||||
successRedirectUrl?: string;
|
||||
forwardParamsSuccessRedirect?: boolean;
|
||||
metadata?: Record<string, any>;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface GetEventTypesResponse {
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface UserProfile {
|
||||
id: number;
|
||||
isPlatform: boolean;
|
||||
};
|
||||
metadata?: Record<string, any>;
|
||||
metadata?: Record<string, unknown>;
|
||||
brandColor?: string;
|
||||
darkBrandColor?: string;
|
||||
theme?: string;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Platform } from "react-native";
|
||||
|
||||
import type { UserProfile } from "./types/users.types";
|
||||
|
||||
export interface WebSessionInfo {
|
||||
isLoggedIn: boolean;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
userInfo?: any;
|
||||
userInfo?: UserProfile;
|
||||
}
|
||||
|
||||
export class WebAuthService {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"types": ["chrome"]
|
||||
},
|
||||
"include": ["extension/**/*.ts", "extension/**/*.tsx", "services/types/**/*.ts", "types/**/*.ts"],
|
||||
"exclude": ["node_modules", ".output"]
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"types": ["nativewind/types"]
|
||||
"types": ["nativewind/types"],
|
||||
"strict": true
|
||||
},
|
||||
"extends": "expo/tsconfig.base",
|
||||
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts", "nativewind-env.d.ts"],
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { Booking } from "../services/types/bookings.types";
|
||||
|
||||
/**
|
||||
* Attendee type extracted from Booking for reuse across the codebase.
|
||||
* This provides a proper type for individual attendees instead of using `any`.
|
||||
*/
|
||||
export type Attendee = NonNullable<Booking["attendees"]>[number];
|
||||
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Ambient declarations for browser extension APIs
|
||||
* These provide proper typing for Firefox/Safari browser namespace
|
||||
* and Brave-specific navigator properties used in oauthService.ts
|
||||
*/
|
||||
|
||||
// Firefox/Safari use 'browser' namespace instead of 'chrome'
|
||||
// This is a WebExtension API that mirrors chrome.* APIs
|
||||
// Firefox/Safari browser.identity API is Promise-based (unlike Chrome's callback-based API)
|
||||
declare const browser:
|
||||
| {
|
||||
identity?: {
|
||||
launchWebAuthFlow: (options: {
|
||||
url: string;
|
||||
interactive: boolean;
|
||||
}) => Promise<string | undefined>;
|
||||
};
|
||||
runtime?: {
|
||||
lastError?: { message?: string };
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
|
||||
// Brave adds isBrave() to navigator
|
||||
interface Navigator {
|
||||
brave?: {
|
||||
isBrave: () => Promise<boolean>;
|
||||
};
|
||||
}
|
||||
@@ -24,9 +24,8 @@ export function createShadow(config: ShadowConfig = {}): ViewStyle {
|
||||
.toString(16)
|
||||
.padStart(2, "0");
|
||||
return {
|
||||
// @ts-ignore - web-only property
|
||||
boxShadow: `${offsetX}px ${offsetY}px ${radius}px rgba(0, 0, 0, ${opacity})`,
|
||||
};
|
||||
} as ViewStyle;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -10,11 +10,7 @@ export const slugify = (str: string, forDisplayingInput?: boolean) => {
|
||||
.toLowerCase() // Convert to lowercase
|
||||
.trim() // Remove whitespace from both sides
|
||||
.normalize("NFD") // Normalize to decomposed form for handling accents
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
.replace(/\p{Diacritic}/gu, "") // Remove any diacritics (accents) from characters
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
.replace(/[^.\p{L}\p{N}\p{Zs}\p{Emoji}]+/gu, "-") // Replace any non-alphanumeric characters (including Unicode and except "." period) with a dash
|
||||
.replace(/[\s_#]+/g, "-") // Replace whitespace, # and underscores with a single dash
|
||||
.replace(/^-+/, "") // Remove dashes from start
|
||||
|
||||
Reference in New Issue
Block a user