diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index 64f7acefd2..b8cbc95a1d 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -59,6 +59,20 @@ export default defineConfig( ], }, }, + // Folders that have been cleaned up and are now conformant - stricter rules enforced here + { + files: ['src/saas/**/*.{js,mjs,jsx,ts,tsx}'], + languageOptions: { + parserOptions: { + project: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-unnecessary-type-assertion': 'error', + }, + }, // Config for browser scripts { files: srcGlobs, diff --git a/frontend/src/global.d.ts b/frontend/src/global.d.ts index c09d1b3a9b..09487d4a06 100644 --- a/frontend/src/global.d.ts +++ b/frontend/src/global.d.ts @@ -12,6 +12,12 @@ declare module 'assets/material-symbols-icons.json' { export default value; } +declare global { + interface Window { + __STIRLING_PDF_BASE_URL__?: string; + } +} + declare module 'axios' { export interface AxiosRequestConfig<_D = unknown> { suppressErrorToast?: boolean; diff --git a/frontend/src/saas/auth/UseSession.tsx b/frontend/src/saas/auth/UseSession.tsx index 0de2231676..fd46263694 100644 --- a/frontend/src/saas/auth/UseSession.tsx +++ b/frontend/src/saas/auth/UseSession.tsx @@ -114,7 +114,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { setSubscription(subscriptionInfo) console.debug('[Auth Debug] Credits fetched successfully:', credits) - } catch (error: any) { + } catch (error: unknown) { console.debug('[Auth Debug] Failed to fetch credits:', error) // Don't set error state for credit fetching failures to avoid disrupting auth flow // Credits might not be available in all deployments @@ -149,7 +149,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { setIsPro(isProUser) console.debug('[Auth Debug] Pro status fetched:', isProUser) } - } catch (error: any) { + } catch (error: unknown) { console.debug('[Auth Debug] Failed to fetch pro status:', error) setIsPro(false) // Default to false if there's an error } @@ -206,7 +206,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { } else { setTrialStatus(null) } - } catch (error: any) { + } catch (error: unknown) { console.debug('[Auth Debug] Failed to fetch trial status:', error) setTrialStatus(null) } @@ -243,7 +243,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { setProfilePictureUrl(data.signedUrl) console.debug('[Auth Debug] Profile picture URL fetched successfully') } - } catch (error: any) { + } catch (error: unknown) { console.debug('[Auth Debug] Failed to fetch profile picture:', error) setProfilePictureUrl(null) } @@ -267,7 +267,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { const metadata = await getProfilePictureMetadata(currentSession.user.id) setProfilePictureMetadata(metadata) console.debug('[Auth Debug] Profile picture metadata fetched:', metadata) - } catch (error: any) { + } catch (error: unknown) { console.debug('[Auth Debug] Failed to fetch profile picture metadata:', error) setProfilePictureMetadata(null) } diff --git a/frontend/src/saas/auth/supabase.ts b/frontend/src/saas/auth/supabase.ts index b8b473f785..338c5c8adc 100644 --- a/frontend/src/saas/auth/supabase.ts +++ b/frontend/src/saas/auth/supabase.ts @@ -83,7 +83,7 @@ export const signInAnonymously = async () => { // Account linking functions export const linkEmailIdentity = async (email: string, password?: string) => { try { - const updateData: any = { email } + const updateData: { email: string; password?: string } = { email } if (password) { updateData.password = password } @@ -143,7 +143,7 @@ export const linkOAuthIdentity = async (provider: 'google' | 'github' | 'apple' } // Helper function to check if user is anonymous -export const isUserAnonymous = (user: any) => { +export const isUserAnonymous = (user: { is_anonymous?: boolean }) => { return user?.is_anonymous === true } diff --git a/frontend/src/saas/components/shared/ManageBillingButton.tsx b/frontend/src/saas/components/shared/ManageBillingButton.tsx index 507aeb07e0..3f9558bbf3 100644 --- a/frontend/src/saas/components/shared/ManageBillingButton.tsx +++ b/frontend/src/saas/components/shared/ManageBillingButton.tsx @@ -38,16 +38,16 @@ export function ManageBillingButton({ setLoading(true); setErr(null); try { - const { data, error } = await supabase.functions.invoke('manage-billing', { + const { data, error } = await supabase.functions.invoke<{ url: string; error?: string }>('manage-billing', { body: { name: 'Functions', return_url: returnUrl}, }) if (error) throw error; - if (!data || 'error' in data) throw new Error((data as any)?.error ?? 'No portal URL'); - window.location.href = (data as any).url; - } catch (e: any) { - setErr(e.message ?? 'Could not open billing portal'); + if (!data || 'error' in data) throw new Error(data?.error ?? 'No portal URL'); + window.location.href = data.url; + } catch (e: unknown) { + setErr(e instanceof Error ? e.message : 'Could not open billing portal'); } finally { setLoading(false); } diff --git a/frontend/src/saas/components/shared/StripeCheckoutSaas.tsx b/frontend/src/saas/components/shared/StripeCheckoutSaas.tsx index 61c164b236..27f1f1abc3 100644 --- a/frontend/src/saas/components/shared/StripeCheckoutSaas.tsx +++ b/frontend/src/saas/components/shared/StripeCheckoutSaas.tsx @@ -24,7 +24,7 @@ interface StripeCheckoutProps { currency?: string; isTrialConversion?: boolean; // Proprietary-specific props (for compatibility) - planGroup?: any; + planGroup?: unknown; minimumSeats?: number; onLicenseActivated?: (licenseInfo: {licenseType: string; enabled: boolean; maxUsers: number; hasKey: boolean}) => void; hostedCheckoutSuccess?: { diff --git a/frontend/src/saas/components/shared/charts/utils/d3Utils.ts b/frontend/src/saas/components/shared/charts/utils/d3Utils.ts index 44b5b50988..ddeed92197 100644 --- a/frontend/src/saas/components/shared/charts/utils/d3Utils.ts +++ b/frontend/src/saas/components/shared/charts/utils/d3Utils.ts @@ -162,7 +162,7 @@ export function createScale(domain: [number, number], range: [number, number]) { * @param wait The wait time in milliseconds * @returns Debounced function */ -export function debounce any>( +export function debounce unknown>( func: T, wait: number ): (...args: Parameters) => void { diff --git a/frontend/src/saas/components/shared/config/configSections/Overview.tsx b/frontend/src/saas/components/shared/config/configSections/Overview.tsx index 90e89c6768..d535fdc2a7 100644 --- a/frontend/src/saas/components/shared/config/configSections/Overview.tsx +++ b/frontend/src/saas/components/shared/config/configSections/Overview.tsx @@ -149,8 +149,8 @@ const Overview: React.FC = ({ onLogoutClick }) => { // Clear success message after 3 seconds setTimeout(() => setSuccess(null), 3000); - } catch (error: any) { - setProfileError(error.message || 'Failed to switch to custom picture'); + } catch (error: unknown) { + setProfileError(error instanceof Error ? error.message : 'Failed to switch to custom picture'); } finally { setProfileUploading(false); } @@ -181,8 +181,8 @@ const Overview: React.FC = ({ onLogoutClick }) => { setSuccess('Account upgraded successfully! You can now sign in with your email.'); setEmail(''); setPassword(''); - } catch (err: any) { - setUpgradeError(err?.message || 'Failed to upgrade account'); + } catch (err: unknown) { + setUpgradeError(err instanceof Error ? err.message : 'Failed to upgrade account'); } finally { setIsLoading(false); } @@ -404,7 +404,7 @@ const Overview: React.FC = ({ onLogoutClick }) => { style={{ width: 16, height: 16 }} /> } - onClick={() => handleOAuthUpgrade(provider.id as any)} + onClick={() => handleOAuthUpgrade(provider.id as 'github' | 'google' | 'apple' | 'azure')} disabled={isLoading} > {provider.label} diff --git a/frontend/src/saas/components/shared/config/configSections/PasswordSecurity.tsx b/frontend/src/saas/components/shared/config/configSections/PasswordSecurity.tsx index e0ff18ddc4..55ab3cdc2b 100644 --- a/frontend/src/saas/components/shared/config/configSections/PasswordSecurity.tsx +++ b/frontend/src/saas/components/shared/config/configSections/PasswordSecurity.tsx @@ -55,8 +55,8 @@ const PasswordSecurity: React.FC = () => { setOpened(false); setDidUpdate(false); }, 2000); - } catch (e: any) { - setError(e?.message || 'Failed to change password'); + } catch (e: unknown) { + setError(e instanceof Error ? e.message : 'Failed to change password'); } finally { setIsLoading(false); } diff --git a/frontend/src/saas/components/shared/config/configSections/apiKeys/hooks/useApiKey.ts b/frontend/src/saas/components/shared/config/configSections/apiKeys/hooks/useApiKey.ts index 22e4c51714..870700eb26 100644 --- a/frontend/src/saas/components/shared/config/configSections/apiKeys/hooks/useApiKey.ts +++ b/frontend/src/saas/components/shared/config/configSections/apiKeys/hooks/useApiKey.ts @@ -1,8 +1,11 @@ import { useCallback, useEffect, useState } from "react"; +import { isAxiosError } from "axios"; import apiClient from "@app/services/apiClient"; import { useAuth } from "@app/auth/UseSession"; import { isUserAnonymous } from "@app/auth/supabase"; +type ApiKeyResponse = string | { apiKey?: string }; + export function useApiKey() { const { session, loading, user } = useAuth(); const isAnonymous = Boolean(user && isUserAnonymous(user)); @@ -17,24 +20,21 @@ export function useApiKey() { setError(null); try { // Backend is POST for get and update - const res = await apiClient.post("/api/v1/user/get-api-key"); - const value = typeof res.data === "string" ? res.data : res.data?.apiKey; + const res = await apiClient.post("/api/v1/user/get-api-key"); + const value = typeof res.data === "string" ? res.data : res.data.apiKey; if (typeof value === "string") setApiKey(value); - } catch (e: any) { + } catch (e: unknown) { // If not found, try to create one by calling update endpoint - if (e?.response?.status === 404) { + if (isAxiosError(e) && e.response?.status === 404) { try { - const createRes = await apiClient.post("/api/v1/user/update-api-key"); - const created = - typeof createRes.data === "string" - ? createRes.data - : createRes.data?.apiKey; + const createRes = await apiClient.post("/api/v1/user/update-api-key"); + const created = typeof createRes.data === "string" ? createRes.data : createRes.data.apiKey; if (typeof created === "string") setApiKey(created); - } catch (createErr: any) { - setError(createErr); + } catch (createErr: unknown) { + setError(createErr instanceof Error ? createErr : new Error(String(createErr))); } } else { - setError(e); + setError(e instanceof Error ? e : new Error(String(e))); } } finally { setIsLoading(false); @@ -46,11 +46,11 @@ export function useApiKey() { setIsRefreshing(true); setError(null); try { - const res = await apiClient.post("/api/v1/user/update-api-key"); - const value = typeof res.data === "string" ? res.data : res.data?.apiKey; + const res = await apiClient.post("/api/v1/user/update-api-key"); + const value = typeof res.data === "string" ? res.data : res.data.apiKey; if (typeof value === "string") setApiKey(value); - } catch (e: any) { - setError(e); + } catch (e: unknown) { + setError(e instanceof Error ? e : new Error(String(e))); } finally { setIsRefreshing(false); } diff --git a/frontend/src/saas/components/shared/config/configSections/apiKeys/hooks/useCredits.ts b/frontend/src/saas/components/shared/config/configSections/apiKeys/hooks/useCredits.ts index 1b52e1b0f3..20c0a93866 100644 --- a/frontend/src/saas/components/shared/config/configSections/apiKeys/hooks/useCredits.ts +++ b/frontend/src/saas/components/shared/config/configSections/apiKeys/hooks/useCredits.ts @@ -6,10 +6,10 @@ import { isUserAnonymous } from "@app/auth/supabase"; function coerceNumber(value: unknown, fallback = 0): number { const n = typeof value === "string" ? Number(value) : (value as number); - return Number.isFinite(n) ? (n as number) : fallback; + return Number.isFinite(n) ? n : fallback; } -function normalizeCredits(raw: any): ApiCredits { +function normalizeCredits(raw: Record): ApiCredits { // Accept a variety of possible backend keys to be resilient return { weeklyCreditsRemaining: coerceNumber( @@ -28,9 +28,9 @@ function normalizeCredits(raw: any): ApiCredits { raw?.totalAvailableCredits ?? raw?.totalRemaining ?? raw?.available_total ), weeklyResetDate: - (raw?.weeklyResetDate ?? raw?.weeklyReset ?? raw?.reset_date) || "", + String(raw?.weeklyResetDate ?? raw?.weeklyReset ?? raw?.reset_date ?? ""), lastApiUsage: - (raw?.lastApiUsage ?? raw?.lastApiUse ?? raw?.last_used_at) || "", + String(raw?.lastApiUsage ?? raw?.lastApiUse ?? raw?.last_used_at ?? ""), }; } @@ -46,7 +46,7 @@ export function useCredits() { setIsLoading(true); setError(null); try { - const res = await apiClient.get("/api/v1/credits"); + const res = await apiClient.get>("/api/v1/credits"); const normalized = normalizeCredits(res.data); // If backend returns an "empty" payload, keep data null so the UI stays in loading/skeleton const isEmpty = !normalized.weeklyCreditsAllocated && @@ -57,8 +57,8 @@ export function useCredits() { !normalized.weeklyResetDate && !normalized.lastApiUsage; setData(isEmpty ? null : normalized); - } catch (e: any) { - setError(e); + } catch (e: unknown) { + setError(e instanceof Error ? e : new Error(String(e))); } finally { setIsLoading(false); setHasAttempted(true); diff --git a/frontend/src/saas/components/shared/config/configSections/plan/AvailablePlansSection.tsx b/frontend/src/saas/components/shared/config/configSections/plan/AvailablePlansSection.tsx index 25f4322f0a..c8fd19321a 100644 --- a/frontend/src/saas/components/shared/config/configSections/plan/AvailablePlansSection.tsx +++ b/frontend/src/saas/components/shared/config/configSections/plan/AvailablePlansSection.tsx @@ -7,9 +7,9 @@ import PlanCard from '@app/components/shared/config/configSections/plan/PlanCard interface AvailablePlansSectionProps { plans: PlanTier[]; currentPlan?: PlanTier; - currentLicenseInfo?: any; - onUpgradeClick: (plan: PlanTier | any) => void; // Accept PlanTierGroup for compatibility - onManageClick?: (plan: PlanTier | any) => void; // Accept PlanTierGroup for compatibility + currentLicenseInfo?: unknown; + onUpgradeClick: (plan: PlanTier) => void; + onManageClick?: (plan: PlanTier) => void; currency?: string; onCurrencyChange?: (currency: string) => void; currencyOptions?: Array<{ value: string; label: string }>; diff --git a/frontend/src/saas/components/shared/config/configSections/plan/PlanCard.tsx b/frontend/src/saas/components/shared/config/configSections/plan/PlanCard.tsx index 9551f358fb..de02c959ab 100644 --- a/frontend/src/saas/components/shared/config/configSections/plan/PlanCard.tsx +++ b/frontend/src/saas/components/shared/config/configSections/plan/PlanCard.tsx @@ -5,15 +5,15 @@ import { PlanTier } from '@app/hooks/usePlans'; interface PlanCardProps { plan?: PlanTier; - planGroup?: any; // For proprietary PlanTierGroup compatibility + planGroup?: { monthly?: PlanTier; yearly?: PlanTier }; // For proprietary PlanTierGroup compatibility isCurrentPlan?: boolean; isCurrentTier?: boolean; isDowngrade?: boolean; isUserProOrAbove?: boolean; - currentLicenseInfo?: any; + currentLicenseInfo?: unknown; currentTier?: string | null; // Accept null for proprietary compatibility - onUpgradeClick?: (plan: any) => void; // Accept PlanTierGroup or PlanTier - onManageClick?: (plan: any) => void; + onUpgradeClick?: (plan: PlanTier) => void; + onManageClick?: (plan: PlanTier) => void; loginEnabled?: boolean; } @@ -31,7 +31,7 @@ const PlanCard: React.FC = ({ loginEnabled: _loginEnabled }) => { // Use plan from props, or extract from planGroup if proprietary is using it - const plan = propPlan || (planGroup as any)?.monthly || (planGroup as any)?.yearly; + const plan = propPlan || planGroup?.monthly || planGroup?.yearly; const { t } = useTranslation(); if (!plan) return null; // Safety check diff --git a/frontend/src/saas/constants/app.ts b/frontend/src/saas/constants/app.ts index dbe845aaed..90a130c76c 100644 --- a/frontend/src/saas/constants/app.ts +++ b/frontend/src/saas/constants/app.ts @@ -5,11 +5,10 @@ export * from '@core/constants/app'; // Get base URL with fallback (for use outside React components) export const getBaseUrl = (): string => { // Try to get from window object if set by app config - const baseUrl = (window as any).__STIRLING_PDF_BASE_URL__ || window.location.origin; - return baseUrl; + return window.__STIRLING_PDF_BASE_URL__ || window.location.origin; }; // Helper to set base URL (to be called when app config loads) export const setBaseUrl = (url: string): void => { - (window as any).__STIRLING_PDF_BASE_URL__ = url; + window.__STIRLING_PDF_BASE_URL__ = url; }; diff --git a/frontend/src/saas/hooks/useEndpointConfig.ts b/frontend/src/saas/hooks/useEndpointConfig.ts index 5da0fdd2af..c98dec5966 100644 --- a/frontend/src/saas/hooks/useEndpointConfig.ts +++ b/frontend/src/saas/hooks/useEndpointConfig.ts @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { isAxiosError } from 'axios'; import apiClient from '@app/services/apiClient'; import type { EndpointAvailabilityDetails } from '@app/types/endpointAvailability'; @@ -153,9 +154,9 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): { setEndpointStatus(fullStatus.status); setEndpointDetails(prev => ({ ...prev, ...fullStatus.details })); globalFetchedSets.add(endpointsKey); - } catch (err: any) { + } catch (err: unknown) { // On 401 (auth error), use optimistic fallback instead of disabling - if (err.response?.status === 401) { + if (isAxiosError(err) && err.response?.status === 401) { console.warn('[useEndpointConfig] 401 error - using optimistic fallback'); const optimisticStatus = endpoints.reduce( (acc, endpoint) => { diff --git a/frontend/src/saas/routes/AuthCallback.tsx b/frontend/src/saas/routes/AuthCallback.tsx index 8738a64509..7b596d7858 100644 --- a/frontend/src/saas/routes/AuthCallback.tsx +++ b/frontend/src/saas/routes/AuthCallback.tsx @@ -7,7 +7,7 @@ import { withBasePath } from '@app/constants/app' interface CallbackState { status: 'processing' | 'success' | 'error' message: string - details?: Record + details?: Record } export default function AuthCallback() { diff --git a/frontend/src/saas/routes/Login.tsx b/frontend/src/saas/routes/Login.tsx index 9347cfeef9..6a0109d583 100644 --- a/frontend/src/saas/routes/Login.tsx +++ b/frontend/src/saas/routes/Login.tsx @@ -68,7 +68,7 @@ export default function Login() { const redirectTo = absoluteWithBasePath('/auth/callback') console.log(`[Login] Signing in with ${provider}`) - const oauthOptions: any = { redirectTo } + const oauthOptions: { redirectTo: string; queryParams?: Record } = { redirectTo } if (provider === 'apple') { oauthOptions.queryParams = { scope: 'email name' } } else if (provider === 'azure') { @@ -175,12 +175,9 @@ export default function Login() { setError(null) console.log('[Login] Signing in anonymously') - const { data, error } = await signInAnonymously() + const { data } = await signInAnonymously() - if (error) { - console.error('[Login] Anonymous sign in error:', error) - setError((error as any)?.message || 'Unknown error') - } else if (data.user) { + if (data.user) { console.log('[Login] Anonymous sign in successful, refreshing session...') // Refresh session to ensure backend endpoints are properly synchronized diff --git a/frontend/src/saas/routes/Signup.tsx b/frontend/src/saas/routes/Signup.tsx index 94df229650..24a83c0c5f 100644 --- a/frontend/src/saas/routes/Signup.tsx +++ b/frontend/src/saas/routes/Signup.tsx @@ -62,12 +62,9 @@ export default function Signup() { setError(null) console.log('[Signup] Initiating anonymous sign-in...') - const { data, error } = await signInAnonymously() + const { data } = await signInAnonymously() - if (error) { - console.error('[Signup] Anonymous sign-in error:', error) - setError(`Failed to create guest account: ${(error as any)?.message || 'Unknown error'}`) - } else if (data.user) { + if (data.user) { console.log('[Signup] Anonymous sign-in successful, refreshing session...') // Refresh session to ensure backend endpoints are properly synchronized diff --git a/frontend/src/saas/routes/login/OAuthButtons.tsx b/frontend/src/saas/routes/login/OAuthButtons.tsx index aeb6c337e6..e53bd3cf06 100644 --- a/frontend/src/saas/routes/login/OAuthButtons.tsx +++ b/frontend/src/saas/routes/login/OAuthButtons.tsx @@ -32,7 +32,7 @@ export default function OAuthButtons({ onProviderClick, isSubmitting, layout = ' position="top" >