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:
Dhairyashil Shinde
2025-12-26 17:13:50 +00:00
committed by GitHub
parent ab512ea263
commit 18017b5320
26 changed files with 243 additions and 74 deletions
+8 -4
View File
@@ -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")}`;
+22 -5
View File
@@ -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");
+19
View File
@@ -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
+15 -6
View File
@@ -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[];
}
+1 -1
View File
@@ -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 },
+8
View File
@@ -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",
+4 -5
View File
@@ -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[];
}
}
+4 -6
View File
@@ -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";
}
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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;
+3 -1
View File
@@ -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 {
+18
View File
@@ -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"]
}
+2 -1
View File
@@ -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"],
+7
View File
@@ -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];
+29
View File
@@ -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>;
};
}
+1 -2
View File
@@ -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 {
-4
View File
@@ -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