Fix any type usage in the saas/ folder (#5934)

# Description of Changes
Ages ago I made #4835 to try and fix all the `any` type usage in the
system but never got it finished, and there were just too many to review
and ensure it still worked. There's even more now.

My new tactic is to fix folder by folder. This fixes the `any` typing in
the `saas/` folder, and also enables `no-unnecessary-type-assertion`,
which really helps reduce pointless `as` casts that AI generates when
the type is already known. I hope to expand both of these to the rest of
the folders soon, but one folder is better than none.
This commit is contained in:
James Brunton
2026-03-16 11:51:16 +00:00
committed by GitHub
parent 1722733802
commit dbff05814f
22 changed files with 123 additions and 112 deletions
@@ -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);
}
@@ -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?: {
@@ -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<T extends (...args: any[]) => any>(
export function debounce<T extends (...args: unknown[]) => unknown>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
@@ -149,8 +149,8 @@ const Overview: React.FC<OverviewProps> = ({ 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<OverviewProps> = ({ 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<OverviewProps> = ({ 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}
@@ -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);
}
@@ -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<ApiKeyResponse>("/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<ApiKeyResponse>("/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<ApiKeyResponse>("/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);
}
@@ -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<string, unknown>): 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<Record<string, unknown>>("/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);
@@ -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 }>;
@@ -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<PlanCardProps> = ({
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