diff --git a/AdminDashboard/src/Pages/Projects/View/Delete.tsx b/AdminDashboard/src/Pages/Projects/View/Delete.tsx index 253ad52d67..d645bc0527 100644 --- a/AdminDashboard/src/Pages/Projects/View/Delete.tsx +++ b/AdminDashboard/src/Pages/Projects/View/Delete.tsx @@ -1,3 +1,4 @@ +import AdminModelAPI from "../../../Utils/ModelAPI"; import PageMap from "../../../Utils/PageMap"; import RouteMap, { RouteUtil } from "../../../Utils/RouteMap"; import Route from "Common/Types/API/Route"; @@ -17,6 +18,7 @@ const DeletePage: FunctionComponent = (): ReactElement => { modelId={modelId} modelNameField="name" modelType={Project} + modelAPI={AdminModelAPI} title={"Project"} breadcrumbLinks={[ { @@ -41,6 +43,7 @@ const DeletePage: FunctionComponent = (): ReactElement => { { Navigation.navigate(RouteMap[PageMap.PROJECTS] as Route); }} diff --git a/AdminDashboard/src/Pages/Projects/View/Index.tsx b/AdminDashboard/src/Pages/Projects/View/Index.tsx index cc758e2183..859346ef56 100644 --- a/AdminDashboard/src/Pages/Projects/View/Index.tsx +++ b/AdminDashboard/src/Pages/Projects/View/Index.tsx @@ -1,3 +1,4 @@ +import AdminModelAPI from "../../../Utils/ModelAPI"; import ObjectID from "Common/Types/ObjectID"; import PageMap from "../../../Utils/PageMap"; import RouteMap, { RouteUtil } from "../../../Utils/RouteMap"; @@ -19,6 +20,7 @@ const Projects: FunctionComponent = (): ReactElement => { modelId={modelId} modelNameField="name" modelType={Project} + modelAPI={AdminModelAPI} title={"Project"} breadcrumbLinks={[ { @@ -43,6 +45,7 @@ const Projects: FunctionComponent = (): ReactElement => {
name="Project" + modelAPI={AdminModelAPI} cardProps={{ title: "Project", description: "Project details", diff --git a/AdminDashboard/src/Pages/Users/View/Delete.tsx b/AdminDashboard/src/Pages/Users/View/Delete.tsx index 4f040e6606..1ee9d61917 100644 --- a/AdminDashboard/src/Pages/Users/View/Delete.tsx +++ b/AdminDashboard/src/Pages/Users/View/Delete.tsx @@ -1,3 +1,4 @@ +import AdminModelAPI from "../../../Utils/ModelAPI"; import PageMap from "../../../Utils/PageMap"; import RouteMap, { RouteUtil } from "../../../Utils/RouteMap"; import Route from "Common/Types/API/Route"; @@ -17,6 +18,7 @@ const DeletePage: FunctionComponent = (): ReactElement => { modelId={modelId} modelNameField="email" modelType={User} + modelAPI={AdminModelAPI} title={"User"} breadcrumbLinks={[ { @@ -39,6 +41,7 @@ const DeletePage: FunctionComponent = (): ReactElement => { { Navigation.navigate(RouteMap[PageMap.USERS] as Route); }} diff --git a/AdminDashboard/src/Pages/Users/View/Index.tsx b/AdminDashboard/src/Pages/Users/View/Index.tsx index 99f82a7f62..d0eef78b5a 100644 --- a/AdminDashboard/src/Pages/Users/View/Index.tsx +++ b/AdminDashboard/src/Pages/Users/View/Index.tsx @@ -1,3 +1,4 @@ +import AdminModelAPI from "../../../Utils/ModelAPI"; import ObjectID from "Common/Types/ObjectID"; import PageMap from "../../../Utils/PageMap"; import RouteMap, { RouteUtil } from "../../../Utils/RouteMap"; @@ -19,6 +20,7 @@ const Users: FunctionComponent = (): ReactElement => { modelId={modelId} modelNameField="email" modelType={User} + modelAPI={AdminModelAPI} title={"User"} breadcrumbLinks={[ { @@ -41,6 +43,7 @@ const Users: FunctionComponent = (): ReactElement => {
name="User" + modelAPI={AdminModelAPI} cardProps={{ title: "User", description: "User details", diff --git a/AdminDashboard/src/Pages/Users/View/Settings.tsx b/AdminDashboard/src/Pages/Users/View/Settings.tsx index e1ce2af64f..c52db53a98 100644 --- a/AdminDashboard/src/Pages/Users/View/Settings.tsx +++ b/AdminDashboard/src/Pages/Users/View/Settings.tsx @@ -1,3 +1,4 @@ +import AdminModelAPI from "../../../Utils/ModelAPI"; import PageMap from "../../../Utils/PageMap"; import RouteMap, { RouteUtil } from "../../../Utils/RouteMap"; import Route from "Common/Types/API/Route"; @@ -19,6 +20,7 @@ const UserSettings: FunctionComponent = (): ReactElement => { modelId={modelId} modelNameField="email" modelType={User} + modelAPI={AdminModelAPI} title={"User"} breadcrumbLinks={[ { @@ -52,6 +54,7 @@ const UserSettings: FunctionComponent = (): ReactElement => { > name="user-master-admin-settings" + modelAPI={AdminModelAPI} cardProps={{ title: "Master Admin Access", description: diff --git a/Common/UI/Components/ModelDelete/ModelDelete.tsx b/Common/UI/Components/ModelDelete/ModelDelete.tsx index 1b0c2f5c13..3ef2700c0c 100644 --- a/Common/UI/Components/ModelDelete/ModelDelete.tsx +++ b/Common/UI/Components/ModelDelete/ModelDelete.tsx @@ -12,6 +12,7 @@ import React, { ReactElement, useState } from "react"; export interface ComponentProps { modelType: { new (): TBaseModel }; modelId: ObjectID; + modelAPI?: typeof ModelAPI | undefined; onDeleteSuccess: () => void; } @@ -29,7 +30,9 @@ const ModelDelete: ( const deleteItem: PromiseVoidFunction = async (): Promise => { setIsLoading(true); try { - await ModelAPI.deleteItem({ + const modelAPI: typeof ModelAPI = props.modelAPI || ModelAPI; + + await modelAPI.deleteItem({ modelType: props.modelType, id: props.modelId, }); diff --git a/Common/UI/Components/ModelDetail/CardModelDetail.tsx b/Common/UI/Components/ModelDetail/CardModelDetail.tsx index e15fd9f65d..395f58be2c 100644 --- a/Common/UI/Components/ModelDetail/CardModelDetail.tsx +++ b/Common/UI/Components/ModelDetail/CardModelDetail.tsx @@ -1,3 +1,4 @@ +import ModelAPI from "../../Utils/ModelAPI/ModelAPI"; import PermissionUtil from "../../Utils/Permission"; import User from "../../Utils/User"; import Navigation from "../../Utils/Navigation"; @@ -33,6 +34,7 @@ export interface ComponentProps { formFields?: undefined | Fields; className?: string | undefined; name: string; + modelAPI?: typeof ModelAPI | undefined; createEditModalWidth?: ModalWidth | undefined; refresher?: boolean; createOrUpdateApiUrl?: URL | undefined; @@ -130,6 +132,7 @@ const CardModelDetail: ( { setItem(item); if (props.modelDetailProps.onItemLoaded) { @@ -144,6 +147,7 @@ const CardModelDetail: ( title={`Edit ${model.singularName}`} modalWidth={props.createEditModalWidth} + modelAPI={props.modelAPI} onClose={() => { setShowModal(false); }} diff --git a/Common/UI/Components/ModelDetail/ModelDetail.tsx b/Common/UI/Components/ModelDetail/ModelDetail.tsx index 24f47c09a3..3c59216553 100644 --- a/Common/UI/Components/ModelDetail/ModelDetail.tsx +++ b/Common/UI/Components/ModelDetail/ModelDetail.tsx @@ -28,6 +28,7 @@ export interface ComponentProps { fields: Array>; onLoadingChange?: undefined | ((isLoading: boolean) => void); modelId: ObjectID; + modelAPI?: typeof ModelAPI | undefined; onError?: ((error: string) => void) | undefined; onItemLoaded?: (item: TBaseModel) => void | undefined; refresher?: undefined | boolean; @@ -179,7 +180,9 @@ const ModelDetail: ( setOnBeforeFetchData(model); } - const item: TBaseModel | null = await ModelAPI.getItem({ + const modelAPI: typeof ModelAPI = props.modelAPI || ModelAPI; + + const item: TBaseModel | null = await modelAPI.getItem({ modelType: props.modelType, id: props.modelId, select: { diff --git a/Common/UI/Components/Page/ModelPage.tsx b/Common/UI/Components/Page/ModelPage.tsx index f955dece76..495c4e10d1 100644 --- a/Common/UI/Components/Page/ModelPage.tsx +++ b/Common/UI/Components/Page/ModelPage.tsx @@ -20,6 +20,7 @@ export interface ComponentProps { modelType: { new (): TBaseModel }; modelId: ObjectID; modelNameField: string; + modelAPI?: typeof ModelAPI | undefined; } const ModelPage: ( @@ -54,7 +55,9 @@ const ModelPage: ( } as Select; } - const item: TBaseModel | null = await ModelAPI.getItem({ + const modelAPI: typeof ModelAPI = props.modelAPI || ModelAPI; + + const item: TBaseModel | null = await modelAPI.getItem({ modelType: props.modelType, id: props.modelId, select: select as Select, diff --git a/MobileApp/src/App.tsx b/MobileApp/src/App.tsx index 6745c42f69..07d2e2e874 100644 --- a/MobileApp/src/App.tsx +++ b/MobileApp/src/App.tsx @@ -1,9 +1,10 @@ import React from "react"; -import { View, StyleSheet } from "react-native"; +import { View, StyleSheet, ViewStyle } from "react-native"; import { StatusBar } from "expo-status-bar"; import { QueryClient } from "@tanstack/react-query"; import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"; import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister"; +import type { Persister } from "@tanstack/query-persist-client-core"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { ThemeProvider, useTheme } from "./theme"; import { AuthProvider } from "./hooks/useAuth"; @@ -11,7 +12,7 @@ import { ProjectProvider } from "./hooks/useProject"; import RootNavigator from "./navigation/RootNavigator"; import OfflineBanner from "./components/OfflineBanner"; -const queryClient = new QueryClient({ +const queryClient: QueryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours @@ -19,7 +20,7 @@ const queryClient = new QueryClient({ }, }); -const asyncStoragePersister = createAsyncStoragePersister({ +const asyncStoragePersister: Persister = createAsyncStoragePersister({ storage: AsyncStorage, throttleTime: 1000, }); @@ -58,7 +59,7 @@ export default function App(): React.JSX.Element { ); } -const styles = StyleSheet.create({ +const styles: { container: ViewStyle } = StyleSheet.create({ container: { flex: 1, }, diff --git a/MobileApp/src/components/AddNoteModal.tsx b/MobileApp/src/components/AddNoteModal.tsx index bdf38e40fd..e7a8c41a7f 100644 --- a/MobileApp/src/components/AddNoteModal.tsx +++ b/MobileApp/src/components/AddNoteModal.tsx @@ -28,15 +28,15 @@ export default function AddNoteModal({ const { theme } = useTheme(); const [noteText, setNoteText] = useState(""); - const handleSubmit = (): void => { - const trimmed = noteText.trim(); + const handleSubmit: () => void = (): void => { + const trimmed: string = noteText.trim(); if (trimmed) { onSubmit(trimmed); setNoteText(""); } }; - const handleClose = (): void => { + const handleClose: () => void = (): void => { setNoteText(""); onClose(); }; @@ -151,7 +151,7 @@ export default function AddNoteModal({ ); } -const styles = StyleSheet.create({ +const styles: ReturnType = StyleSheet.create({ overlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.6)", diff --git a/MobileApp/src/components/EpisodeCard.tsx b/MobileApp/src/components/EpisodeCard.tsx index dab45d69aa..e5b795495a 100644 --- a/MobileApp/src/components/EpisodeCard.tsx +++ b/MobileApp/src/components/EpisodeCard.tsx @@ -3,7 +3,11 @@ import { View, Text, StyleSheet, TouchableOpacity } from "react-native"; import { useTheme } from "../theme"; import { rgbToHex } from "../utils/color"; import { formatRelativeTime } from "../utils/date"; -import type { IncidentEpisodeItem, AlertEpisodeItem } from "../api/types"; +import type { + IncidentEpisodeItem, + AlertEpisodeItem, + NamedEntityWithColor, +} from "../api/types"; type EpisodeCardProps = | { @@ -23,30 +27,30 @@ export default function EpisodeCard( const { episode, type, onPress } = props; const { theme } = useTheme(); - const state = + const state: NamedEntityWithColor = type === "incident" ? (episode as IncidentEpisodeItem).currentIncidentState : (episode as AlertEpisodeItem).currentAlertState; - const severity = + const severity: NamedEntityWithColor = type === "incident" ? (episode as IncidentEpisodeItem).incidentSeverity : (episode as AlertEpisodeItem).alertSeverity; - const childCount = + const childCount: number = type === "incident" ? (episode as IncidentEpisodeItem).incidentCount : (episode as AlertEpisodeItem).alertCount; - const stateColor = state?.color + const stateColor: string = state?.color ? rgbToHex(state.color) : theme.colors.textTertiary; - const severityColor = severity?.color + const severityColor: string = severity?.color ? rgbToHex(severity.color) : theme.colors.textTertiary; - const timeString = formatRelativeTime( + const timeString: string = formatRelativeTime( (episode as IncidentEpisodeItem).declaredAt || episode.createdAt, ); @@ -121,7 +125,7 @@ export default function EpisodeCard( ); } -const styles = StyleSheet.create({ +const styles: ReturnType = StyleSheet.create({ card: { padding: 16, borderRadius: 12, diff --git a/MobileApp/src/components/OfflineBanner.tsx b/MobileApp/src/components/OfflineBanner.tsx index 144ccb733d..690f315e01 100644 --- a/MobileApp/src/components/OfflineBanner.tsx +++ b/MobileApp/src/components/OfflineBanner.tsx @@ -6,9 +6,9 @@ import { useNetworkStatus } from "../hooks/useNetworkStatus"; export default function OfflineBanner(): React.JSX.Element | null { const { theme } = useTheme(); const { isConnected, isInternetReachable } = useNetworkStatus(); - const slideAnim = useRef(new Animated.Value(-60)).current; + const slideAnim: Animated.Value = useRef(new Animated.Value(-60)).current; - const isOffline = !isConnected || isInternetReachable === false; + const isOffline: boolean = !isConnected || isInternetReachable === false; useEffect(() => { Animated.spring(slideAnim, { @@ -43,7 +43,7 @@ export default function OfflineBanner(): React.JSX.Element | null { ); } -const styles = StyleSheet.create({ +const styles: ReturnType = StyleSheet.create({ container: { position: "absolute", top: 0, diff --git a/MobileApp/src/components/SkeletonCard.tsx b/MobileApp/src/components/SkeletonCard.tsx index c95ccdf871..b563f81bc9 100644 --- a/MobileApp/src/components/SkeletonCard.tsx +++ b/MobileApp/src/components/SkeletonCard.tsx @@ -208,20 +208,22 @@ export default function SkeletonCard({ /> {/* Body lines */} - {Array.from({ length: Math.max(lines - 1, 1) }).map((_: unknown, index: number) => { - return ( - - ); - })} + {Array.from({ length: Math.max(lines - 1, 1) }).map( + (_: unknown, index: number) => { + return ( + + ); + }, + )} ); } diff --git a/MobileApp/src/components/SwipeableCard.tsx b/MobileApp/src/components/SwipeableCard.tsx index 8287879126..ff348c4223 100644 --- a/MobileApp/src/components/SwipeableCard.tsx +++ b/MobileApp/src/components/SwipeableCard.tsx @@ -36,15 +36,21 @@ export default function SwipeableCard({ const translateX: Animated.Value = useRef(new Animated.Value(0)).current; const hasTriggeredHaptic: React.MutableRefObject = useRef(false); - const panResponder = useRef( + const panResponder: PanResponderInstance = useRef( PanResponder.create({ - onMoveShouldSetPanResponder: (_, gestureState) => { + onMoveShouldSetPanResponder: ( + _: GestureResponderEvent, + gestureState: PanResponderGestureState, + ) => { return Math.abs(gestureState.dx) > 10 && Math.abs(gestureState.dy) < 20; }, - onPanResponderMove: (_, gestureState) => { + onPanResponderMove: ( + _: GestureResponderEvent, + gestureState: PanResponderGestureState, + ) => { // Limit swipe range - const maxSwipe = 120; - let dx = gestureState.dx; + const maxSwipe: number = 120; + let dx: number = gestureState.dx; if (!rightAction && dx < 0) { dx = 0; } @@ -62,7 +68,10 @@ export default function SwipeableCard({ hasTriggeredHaptic.current = false; } }, - onPanResponderRelease: (_, gestureState) => { + onPanResponderRelease: ( + _: GestureResponderEvent, + gestureState: PanResponderGestureState, + ) => { if (gestureState.dx > SWIPE_THRESHOLD && leftAction) { leftAction.onAction(); } else if (gestureState.dx < -SWIPE_THRESHOLD && rightAction) { @@ -124,7 +133,7 @@ export default function SwipeableCard({ ); } -const styles = StyleSheet.create({ +const styles: ReturnType = StyleSheet.create({ container: { overflow: "hidden", borderRadius: 12, diff --git a/MobileApp/src/hooks/useAlertDetail.ts b/MobileApp/src/hooks/useAlertDetail.ts index f15e1bd5f9..b06ad020ab 100644 --- a/MobileApp/src/hooks/useAlertDetail.ts +++ b/MobileApp/src/hooks/useAlertDetail.ts @@ -1,11 +1,15 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, UseQueryResult } from "@tanstack/react-query"; import { fetchAlertById, fetchAlertStates, fetchAlertStateTimeline, } from "../api/alerts"; +import type { AlertItem, AlertState, StateTimelineItem } from "../api/types"; -export function useAlertDetail(projectId: string, alertId: string) { +export function useAlertDetail( + projectId: string, + alertId: string, +): UseQueryResult { return useQuery({ queryKey: ["alert", projectId, alertId], queryFn: () => { @@ -15,7 +19,9 @@ export function useAlertDetail(projectId: string, alertId: string) { }); } -export function useAlertStates(projectId: string) { +export function useAlertStates( + projectId: string, +): UseQueryResult { return useQuery({ queryKey: ["alert-states", projectId], queryFn: () => { @@ -25,7 +31,10 @@ export function useAlertStates(projectId: string) { }); } -export function useAlertStateTimeline(projectId: string, alertId: string) { +export function useAlertStateTimeline( + projectId: string, + alertId: string, +): UseQueryResult { return useQuery({ queryKey: ["alert-state-timeline", projectId, alertId], queryFn: () => { diff --git a/MobileApp/src/hooks/useAlertEpisodeDetail.ts b/MobileApp/src/hooks/useAlertEpisodeDetail.ts index 9a86843532..2b28eca9d3 100644 --- a/MobileApp/src/hooks/useAlertEpisodeDetail.ts +++ b/MobileApp/src/hooks/useAlertEpisodeDetail.ts @@ -1,12 +1,21 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, UseQueryResult } from "@tanstack/react-query"; import { fetchAlertEpisodeById, fetchAlertEpisodeStates, fetchAlertEpisodeStateTimeline, fetchAlertEpisodeNotes, } from "../api/alertEpisodes"; +import type { + AlertEpisodeItem, + AlertState, + StateTimelineItem, + NoteItem, +} from "../api/types"; -export function useAlertEpisodeDetail(projectId: string, episodeId: string) { +export function useAlertEpisodeDetail( + projectId: string, + episodeId: string, +): UseQueryResult { return useQuery({ queryKey: ["alert-episode", projectId, episodeId], queryFn: () => { @@ -16,7 +25,9 @@ export function useAlertEpisodeDetail(projectId: string, episodeId: string) { }); } -export function useAlertEpisodeStates(projectId: string) { +export function useAlertEpisodeStates( + projectId: string, +): UseQueryResult { return useQuery({ queryKey: ["alert-states", projectId], queryFn: () => { @@ -29,7 +40,7 @@ export function useAlertEpisodeStates(projectId: string) { export function useAlertEpisodeStateTimeline( projectId: string, episodeId: string, -) { +): UseQueryResult { return useQuery({ queryKey: ["alert-episode-state-timeline", projectId, episodeId], queryFn: () => { @@ -39,7 +50,10 @@ export function useAlertEpisodeStateTimeline( }); } -export function useAlertEpisodeNotes(projectId: string, episodeId: string) { +export function useAlertEpisodeNotes( + projectId: string, + episodeId: string, +): UseQueryResult { return useQuery({ queryKey: ["alert-episode-notes", projectId, episodeId], queryFn: () => { diff --git a/MobileApp/src/hooks/useAlertEpisodes.ts b/MobileApp/src/hooks/useAlertEpisodes.ts index 1c225aded0..2ab0404365 100644 --- a/MobileApp/src/hooks/useAlertEpisodes.ts +++ b/MobileApp/src/hooks/useAlertEpisodes.ts @@ -1,11 +1,12 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, UseQueryResult } from "@tanstack/react-query"; import { fetchAlertEpisodes } from "../api/alertEpisodes"; +import type { ListResponse, AlertEpisodeItem } from "../api/types"; export function useAlertEpisodes( projectId: string, skip: number = 0, limit: number = 20, -) { +): UseQueryResult, Error> { return useQuery({ queryKey: ["alert-episodes", projectId, skip, limit], queryFn: () => { @@ -15,15 +16,18 @@ export function useAlertEpisodes( }); } -export function useUnresolvedAlertEpisodeCount(projectId: string) { +export function useUnresolvedAlertEpisodeCount( + projectId: string, +): UseQueryResult { return useQuery({ queryKey: ["alert-episodes", "unresolved-count", projectId], queryFn: async () => { - const response = await fetchAlertEpisodes(projectId, { - skip: 0, - limit: 1, - unresolvedOnly: true, - }); + const response: ListResponse = + await fetchAlertEpisodes(projectId, { + skip: 0, + limit: 1, + unresolvedOnly: true, + }); return response.count; }, enabled: Boolean(projectId), diff --git a/MobileApp/src/hooks/useAlertNotes.ts b/MobileApp/src/hooks/useAlertNotes.ts index 12a3c5761c..96c2e52576 100644 --- a/MobileApp/src/hooks/useAlertNotes.ts +++ b/MobileApp/src/hooks/useAlertNotes.ts @@ -1,7 +1,11 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, UseQueryResult } from "@tanstack/react-query"; import { fetchAlertNotes } from "../api/alertNotes"; +import type { NoteItem } from "../api/types"; -export function useAlertNotes(projectId: string, alertId: string) { +export function useAlertNotes( + projectId: string, + alertId: string, +): UseQueryResult { return useQuery({ queryKey: ["alert-notes", projectId, alertId], queryFn: () => { diff --git a/MobileApp/src/hooks/useAlerts.ts b/MobileApp/src/hooks/useAlerts.ts index 13bd970cf3..1365e09910 100644 --- a/MobileApp/src/hooks/useAlerts.ts +++ b/MobileApp/src/hooks/useAlerts.ts @@ -1,11 +1,12 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, UseQueryResult } from "@tanstack/react-query"; import { fetchAlerts } from "../api/alerts"; +import type { ListResponse, AlertItem } from "../api/types"; export function useAlerts( projectId: string, skip: number = 0, limit: number = 20, -) { +): UseQueryResult, Error> { return useQuery({ queryKey: ["alerts", projectId, skip, limit], queryFn: () => { @@ -15,11 +16,13 @@ export function useAlerts( }); } -export function useUnresolvedAlertCount(projectId: string) { +export function useUnresolvedAlertCount( + projectId: string, +): UseQueryResult { return useQuery({ queryKey: ["alerts", "unresolved-count", projectId], queryFn: async () => { - const response = await fetchAlerts(projectId, { + const response: ListResponse = await fetchAlerts(projectId, { skip: 0, limit: 1, unresolvedOnly: true, diff --git a/MobileApp/src/hooks/useAuth.tsx b/MobileApp/src/hooks/useAuth.tsx index a462997715..8d34adfcc6 100644 --- a/MobileApp/src/hooks/useAuth.tsx +++ b/MobileApp/src/hooks/useAuth.tsx @@ -27,7 +27,8 @@ interface AuthContextValue { setIsAuthenticated: (value: boolean) => void; } -const AuthContext = createContext(undefined); +const AuthContext: React.Context = + createContext(undefined); interface AuthProviderProps { children: ReactNode; @@ -36,22 +37,23 @@ interface AuthProviderProps { export function AuthProvider({ children, }: AuthProviderProps): React.JSX.Element { - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [needsServerUrl, setNeedsServerUrl] = useState(false); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [needsServerUrl, setNeedsServerUrl] = useState(false); const [user, setUser] = useState(null); - useEffect(() => { + useEffect((): void => { const checkAuth = async (): Promise => { try { - const hasUrl = await hasServerUrl(); + const hasUrl: boolean = await hasServerUrl(); if (!hasUrl) { setNeedsServerUrl(true); setIsLoading(false); return; } - const tokens = await getTokens(); + const tokens: { accessToken: string; refreshToken: string } | null = + await getTokens(); if (tokens?.accessToken) { setIsAuthenticated(true); } @@ -66,8 +68,8 @@ export function AuthProvider({ }, []); // Register auth failure handler for 401 interceptor - useEffect(() => { - setOnAuthFailure(() => { + useEffect((): void => { + setOnAuthFailure((): void => { setIsAuthenticated(false); setUser(null); }); @@ -75,7 +77,7 @@ export function AuthProvider({ const login = useCallback( async (email: string, password: string): Promise => { - const response = await apiLogin(email, password); + const response: LoginResponse = await apiLogin(email, password); if (!response.twoFactorRequired && response.accessToken) { setIsAuthenticated(true); @@ -113,7 +115,7 @@ export function AuthProvider({ } export function useAuth(): AuthContextValue { - const context = useContext(AuthContext); + const context: AuthContextValue | undefined = useContext(AuthContext); if (!context) { throw new Error("useAuth must be used within an AuthProvider"); } diff --git a/MobileApp/src/hooks/useBiometric.ts b/MobileApp/src/hooks/useBiometric.ts index 4d2b297f44..57ed93c2fe 100644 --- a/MobileApp/src/hooks/useBiometric.ts +++ b/MobileApp/src/hooks/useBiometric.ts @@ -14,18 +14,18 @@ interface BiometricState { } export function useBiometric(): BiometricState { - const [isAvailable, setIsAvailable] = useState(false); - const [isEnabled, setIsEnabled] = useState(false); - const [biometricType, setBiometricType] = useState("Biometrics"); + const [isAvailable, setIsAvailable] = useState(false); + const [isEnabled, setIsEnabled] = useState(false); + const [biometricType, setBiometricType] = useState("Biometrics"); - useEffect(() => { + useEffect((): void => { const check = async (): Promise => { - const compatible = await LocalAuthentication.hasHardwareAsync(); - const enrolled = await LocalAuthentication.isEnrolledAsync(); + const compatible: boolean = await LocalAuthentication.hasHardwareAsync(); + const enrolled: boolean = await LocalAuthentication.isEnrolledAsync(); setIsAvailable(compatible && enrolled); if (compatible) { - const types = + const types: LocalAuthentication.AuthenticationType[] = await LocalAuthentication.supportedAuthenticationTypesAsync(); if ( types.includes( @@ -40,7 +40,7 @@ export function useBiometric(): BiometricState { } } - const enabled = await getBiometricEnabled(); + const enabled: boolean = await getBiometricEnabled(); setIsEnabled(enabled); }; @@ -48,7 +48,8 @@ export function useBiometric(): BiometricState { }, []); const authenticate = useCallback(async (): Promise => { - const result = await LocalAuthentication.authenticateAsync({ + const result: LocalAuthentication.LocalAuthenticationResult = + await LocalAuthentication.authenticateAsync({ promptMessage: "Authenticate to access OneUptime", fallbackLabel: "Use passcode", disableDeviceFallback: false, @@ -58,7 +59,8 @@ export function useBiometric(): BiometricState { const setEnabled = useCallback(async (enabled: boolean): Promise => { if (enabled) { - const result = await LocalAuthentication.authenticateAsync({ + const result: LocalAuthentication.LocalAuthenticationResult = + await LocalAuthentication.authenticateAsync({ promptMessage: "Confirm to enable biometric unlock", fallbackLabel: "Use passcode", disableDeviceFallback: false, diff --git a/MobileApp/src/hooks/useHaptics.ts b/MobileApp/src/hooks/useHaptics.ts index 6028a296fe..69fc41b140 100644 --- a/MobileApp/src/hooks/useHaptics.ts +++ b/MobileApp/src/hooks/useHaptics.ts @@ -1,6 +1,14 @@ import * as Haptics from "expo-haptics"; -export function useHaptics() { +interface HapticsResult { + successFeedback: () => Promise; + errorFeedback: () => Promise; + lightImpact: () => Promise; + mediumImpact: () => Promise; + selectionFeedback: () => Promise; +} + +export function useHaptics(): HapticsResult { const successFeedback = async (): Promise => { await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); }; diff --git a/MobileApp/src/hooks/useIncidentDetail.ts b/MobileApp/src/hooks/useIncidentDetail.ts index b5c9541876..43cf75ac35 100644 --- a/MobileApp/src/hooks/useIncidentDetail.ts +++ b/MobileApp/src/hooks/useIncidentDetail.ts @@ -1,11 +1,19 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, UseQueryResult } from "@tanstack/react-query"; import { fetchIncidentById, fetchIncidentStates, fetchIncidentStateTimeline, } from "../api/incidents"; +import type { + IncidentItem, + IncidentState, + StateTimelineItem, +} from "../api/types"; -export function useIncidentDetail(projectId: string, incidentId: string) { +export function useIncidentDetail( + projectId: string, + incidentId: string, +): UseQueryResult { return useQuery({ queryKey: ["incident", projectId, incidentId], queryFn: () => { @@ -15,7 +23,9 @@ export function useIncidentDetail(projectId: string, incidentId: string) { }); } -export function useIncidentStates(projectId: string) { +export function useIncidentStates( + projectId: string, +): UseQueryResult { return useQuery({ queryKey: ["incident-states", projectId], queryFn: () => { @@ -28,7 +38,7 @@ export function useIncidentStates(projectId: string) { export function useIncidentStateTimeline( projectId: string, incidentId: string, -) { +): UseQueryResult { return useQuery({ queryKey: ["incident-state-timeline", projectId, incidentId], queryFn: () => { diff --git a/MobileApp/src/hooks/useIncidentEpisodeDetail.ts b/MobileApp/src/hooks/useIncidentEpisodeDetail.ts index 620b90fb16..9ba177b7a6 100644 --- a/MobileApp/src/hooks/useIncidentEpisodeDetail.ts +++ b/MobileApp/src/hooks/useIncidentEpisodeDetail.ts @@ -1,12 +1,21 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, UseQueryResult } from "@tanstack/react-query"; import { fetchIncidentEpisodeById, fetchIncidentEpisodeStates, fetchIncidentEpisodeStateTimeline, fetchIncidentEpisodeNotes, } from "../api/incidentEpisodes"; +import type { + IncidentEpisodeItem, + IncidentState, + StateTimelineItem, + NoteItem, +} from "../api/types"; -export function useIncidentEpisodeDetail(projectId: string, episodeId: string) { +export function useIncidentEpisodeDetail( + projectId: string, + episodeId: string, +): UseQueryResult { return useQuery({ queryKey: ["incident-episode", projectId, episodeId], queryFn: () => { @@ -16,7 +25,9 @@ export function useIncidentEpisodeDetail(projectId: string, episodeId: string) { }); } -export function useIncidentEpisodeStates(projectId: string) { +export function useIncidentEpisodeStates( + projectId: string, +): UseQueryResult { return useQuery({ queryKey: ["incident-states", projectId], queryFn: () => { @@ -29,7 +40,7 @@ export function useIncidentEpisodeStates(projectId: string) { export function useIncidentEpisodeStateTimeline( projectId: string, episodeId: string, -) { +): UseQueryResult { return useQuery({ queryKey: ["incident-episode-state-timeline", projectId, episodeId], queryFn: () => { @@ -39,7 +50,10 @@ export function useIncidentEpisodeStateTimeline( }); } -export function useIncidentEpisodeNotes(projectId: string, episodeId: string) { +export function useIncidentEpisodeNotes( + projectId: string, + episodeId: string, +): UseQueryResult { return useQuery({ queryKey: ["incident-episode-notes", projectId, episodeId], queryFn: () => { diff --git a/MobileApp/src/hooks/useIncidentEpisodes.ts b/MobileApp/src/hooks/useIncidentEpisodes.ts index 0573bb2354..0f765c3819 100644 --- a/MobileApp/src/hooks/useIncidentEpisodes.ts +++ b/MobileApp/src/hooks/useIncidentEpisodes.ts @@ -1,11 +1,12 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, UseQueryResult } from "@tanstack/react-query"; import { fetchIncidentEpisodes } from "../api/incidentEpisodes"; +import type { ListResponse, IncidentEpisodeItem } from "../api/types"; export function useIncidentEpisodes( projectId: string, skip: number = 0, limit: number = 20, -) { +): UseQueryResult, Error> { return useQuery({ queryKey: ["incident-episodes", projectId, skip, limit], queryFn: () => { @@ -15,15 +16,18 @@ export function useIncidentEpisodes( }); } -export function useUnresolvedIncidentEpisodeCount(projectId: string) { +export function useUnresolvedIncidentEpisodeCount( + projectId: string, +): UseQueryResult { return useQuery({ queryKey: ["incident-episodes", "unresolved-count", projectId], queryFn: async () => { - const response = await fetchIncidentEpisodes(projectId, { - skip: 0, - limit: 1, - unresolvedOnly: true, - }); + const response: ListResponse = + await fetchIncidentEpisodes(projectId, { + skip: 0, + limit: 1, + unresolvedOnly: true, + }); return response.count; }, enabled: Boolean(projectId), diff --git a/MobileApp/src/hooks/useIncidentNotes.ts b/MobileApp/src/hooks/useIncidentNotes.ts index 03a725a4fd..dd12b9ff1e 100644 --- a/MobileApp/src/hooks/useIncidentNotes.ts +++ b/MobileApp/src/hooks/useIncidentNotes.ts @@ -1,7 +1,11 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, UseQueryResult } from "@tanstack/react-query"; import { fetchIncidentNotes } from "../api/incidentNotes"; +import type { NoteItem } from "../api/types"; -export function useIncidentNotes(projectId: string, incidentId: string) { +export function useIncidentNotes( + projectId: string, + incidentId: string, +): UseQueryResult { return useQuery({ queryKey: ["incident-notes", projectId, incidentId], queryFn: () => { diff --git a/MobileApp/src/hooks/useIncidents.ts b/MobileApp/src/hooks/useIncidents.ts index 4ee507141e..2879fc5471 100644 --- a/MobileApp/src/hooks/useIncidents.ts +++ b/MobileApp/src/hooks/useIncidents.ts @@ -1,11 +1,12 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, UseQueryResult } from "@tanstack/react-query"; import { fetchIncidents } from "../api/incidents"; +import type { ListResponse, IncidentItem } from "../api/types"; export function useIncidents( projectId: string, skip: number = 0, limit: number = 20, -) { +): UseQueryResult, Error> { return useQuery({ queryKey: ["incidents", projectId, skip, limit], queryFn: () => { @@ -15,15 +16,20 @@ export function useIncidents( }); } -export function useUnresolvedIncidentCount(projectId: string) { +export function useUnresolvedIncidentCount( + projectId: string, +): UseQueryResult { return useQuery({ queryKey: ["incidents", "unresolved-count", projectId], queryFn: async () => { - const response = await fetchIncidents(projectId, { - skip: 0, - limit: 1, - unresolvedOnly: true, - }); + const response: ListResponse = await fetchIncidents( + projectId, + { + skip: 0, + limit: 1, + unresolvedOnly: true, + }, + ); return response.count; }, enabled: Boolean(projectId), diff --git a/MobileApp/src/hooks/useNetworkStatus.ts b/MobileApp/src/hooks/useNetworkStatus.ts index 434e1bb1ca..d32b149232 100644 --- a/MobileApp/src/hooks/useNetworkStatus.ts +++ b/MobileApp/src/hooks/useNetworkStatus.ts @@ -12,15 +12,17 @@ export function useNetworkStatus(): NetworkStatus { isInternetReachable: true, }); - useEffect(() => { - const unsubscribe = NetInfo.addEventListener((state: NetInfoState) => { - setStatus({ - isConnected: state.isConnected ?? true, - isInternetReachable: state.isInternetReachable, - }); - }); + useEffect((): (() => void) => { + const unsubscribe: (() => void) = NetInfo.addEventListener( + (state: NetInfoState): void => { + setStatus({ + isConnected: state.isConnected ?? true, + isInternetReachable: state.isInternetReachable, + }); + }, + ); - return () => { + return (): void => { unsubscribe(); }; }, []); diff --git a/MobileApp/src/hooks/useProject.tsx b/MobileApp/src/hooks/useProject.tsx index cf6d3ce6a5..77e31dcc36 100644 --- a/MobileApp/src/hooks/useProject.tsx +++ b/MobileApp/src/hooks/useProject.tsx @@ -21,9 +21,8 @@ interface ProjectContextValue { clearProject: () => Promise; } -const ProjectContext = createContext( - undefined, -); +const ProjectContext: React.Context = + createContext(undefined); interface ProjectProviderProps { children: ReactNode; @@ -36,20 +35,23 @@ export function ProjectProvider({ null, ); const [projectList, setProjectList] = useState([]); - const [isLoadingProjects, setIsLoadingProjects] = useState(true); + const [isLoadingProjects, setIsLoadingProjects] = useState(true); const loadProjects = useCallback(async (): Promise => { try { setIsLoadingProjects(true); - const response = await fetchProjects(); + const response: { data: ProjectItem[] } = await fetchProjects(); setProjectList(response.data); // Try to restore previously selected project - const savedId = await AsyncStorage.getItem(PROJECT_STORAGE_KEY); + const savedId: string | null = + await AsyncStorage.getItem(PROJECT_STORAGE_KEY); if (savedId) { - const saved = response.data.find((p: ProjectItem) => { - return p._id === savedId; - }); + const saved: ProjectItem | undefined = response.data.find( + (p: ProjectItem): boolean => { + return p._id === savedId; + }, + ); if (saved) { setSelectedProject(saved); } @@ -57,7 +59,7 @@ export function ProjectProvider({ // Auto-select if only one project if (!savedId && response.data.length === 1) { - const project = response.data[0]!; + const project: ProjectItem = response.data[0]!; setSelectedProject(project); await AsyncStorage.setItem(PROJECT_STORAGE_KEY, project._id); } @@ -68,7 +70,7 @@ export function ProjectProvider({ } }, []); - useEffect(() => { + useEffect((): void => { loadProjects(); }, [loadProjects]); @@ -102,7 +104,7 @@ export function ProjectProvider({ } export function useProject(): ProjectContextValue { - const context = useContext(ProjectContext); + const context: ProjectContextValue | undefined = useContext(ProjectContext); if (!context) { throw new Error("useProject must be used within a ProjectProvider"); } diff --git a/MobileApp/src/hooks/usePushNotifications.ts b/MobileApp/src/hooks/usePushNotifications.ts index d7f030d466..f44a2985da 100644 --- a/MobileApp/src/hooks/usePushNotifications.ts +++ b/MobileApp/src/hooks/usePushNotifications.ts @@ -18,34 +18,35 @@ import { useProject } from "./useProject"; const PUSH_TOKEN_KEY = "oneuptime_expo_push_token"; export function usePushNotifications(navigationRef: unknown): void { - const { isAuthenticated } = useAuth(); - const { projectList } = useProject(); + const { isAuthenticated }: { isAuthenticated: boolean } = useAuth(); + const { projectList }: { projectList: Array<{ _id: string }> } = + useProject(); const responseListenerRef = useRef(null); const receivedListenerRef = useRef(null); // Set up channels and categories on mount - useEffect(() => { + useEffect((): void => { setupNotificationChannels(); setupNotificationCategories(); }, []); // Set navigation ref for deep linking - useEffect(() => { + useEffect((): void => { if (navigationRef) { setNavigationRef(navigationRef); } }, [navigationRef]); // Register push token when authenticated and projects loaded - useEffect(() => { + useEffect((): (() => void) | undefined => { if (!isAuthenticated || projectList.length === 0) { - return; + return undefined; } - let cancelled = false; + let cancelled: boolean = false; const register = async (): Promise => { - const token = await requestPermissionsAndGetToken(); + const token: string | null = await requestPermissionsAndGetToken(); if (!token || cancelled) { return; } @@ -70,18 +71,19 @@ export function usePushNotifications(navigationRef: unknown): void { register(); - return () => { + return (): void => { cancelled = true; }; }, [isAuthenticated, projectList]); // Set up notification listeners - useEffect(() => { - receivedListenerRef.current = Notifications.addNotificationReceivedListener( - (_notification) => { - // Foreground notification received — handler in setup.ts shows it - }, - ); + useEffect((): (() => void) => { + receivedListenerRef.current = + Notifications.addNotificationReceivedListener( + (_notification: Notifications.Notification): void => { + // Foreground notification received — handler in setup.ts shows it + }, + ); responseListenerRef.current = Notifications.addNotificationResponseReceivedListener( @@ -89,13 +91,15 @@ export function usePushNotifications(navigationRef: unknown): void { ); // Handle cold-start: check if app was opened via notification - Notifications.getLastNotificationResponseAsync().then((response) => { - if (response) { - handleNotificationResponse(response); - } - }); + Notifications.getLastNotificationResponseAsync().then( + (response: Notifications.NotificationResponse | null): void => { + if (response) { + handleNotificationResponse(response); + } + }, + ); - return () => { + return (): void => { if (receivedListenerRef.current) { receivedListenerRef.current.remove(); } @@ -108,7 +112,7 @@ export function usePushNotifications(navigationRef: unknown): void { export async function unregisterPushToken(): Promise { try { - const token = await AsyncStorage.getItem(PUSH_TOKEN_KEY); + const token: string | null = await AsyncStorage.getItem(PUSH_TOKEN_KEY); if (token) { await unregisterPushDevice(token); await AsyncStorage.removeItem(PUSH_TOKEN_KEY); diff --git a/MobileApp/src/navigation/AlertEpisodesStackNavigator.tsx b/MobileApp/src/navigation/AlertEpisodesStackNavigator.tsx index e6d376e2fb..3d2d3e3559 100644 --- a/MobileApp/src/navigation/AlertEpisodesStackNavigator.tsx +++ b/MobileApp/src/navigation/AlertEpisodesStackNavigator.tsx @@ -5,7 +5,7 @@ import AlertEpisodesScreen from "../screens/AlertEpisodesScreen"; import AlertEpisodeDetailScreen from "../screens/AlertEpisodeDetailScreen"; import type { AlertEpisodesStackParamList } from "./types"; -const Stack = createNativeStackNavigator(); +const Stack: ReturnType> = createNativeStackNavigator(); export default function AlertEpisodesStackNavigator(): React.JSX.Element { const { theme } = useTheme(); diff --git a/MobileApp/src/navigation/AlertsStackNavigator.tsx b/MobileApp/src/navigation/AlertsStackNavigator.tsx index 56a6e34b9a..53205257b2 100644 --- a/MobileApp/src/navigation/AlertsStackNavigator.tsx +++ b/MobileApp/src/navigation/AlertsStackNavigator.tsx @@ -5,7 +5,7 @@ import AlertsScreen from "../screens/AlertsScreen"; import AlertDetailScreen from "../screens/AlertDetailScreen"; import type { AlertsStackParamList } from "./types"; -const Stack = createNativeStackNavigator(); +const Stack: ReturnType> = createNativeStackNavigator(); export default function AlertsStackNavigator(): React.JSX.Element { const { theme } = useTheme(); diff --git a/MobileApp/src/navigation/AuthStackNavigator.tsx b/MobileApp/src/navigation/AuthStackNavigator.tsx index 4ab83070d6..4d567f22f0 100644 --- a/MobileApp/src/navigation/AuthStackNavigator.tsx +++ b/MobileApp/src/navigation/AuthStackNavigator.tsx @@ -5,7 +5,7 @@ import ServerUrlScreen from "../screens/auth/ServerUrlScreen"; import LoginScreen from "../screens/auth/LoginScreen"; import { useTheme } from "../theme"; -const Stack = createNativeStackNavigator(); +const Stack: ReturnType> = createNativeStackNavigator(); interface AuthStackNavigatorProps { initialRoute: keyof AuthStackParamList; diff --git a/MobileApp/src/navigation/IncidentEpisodesStackNavigator.tsx b/MobileApp/src/navigation/IncidentEpisodesStackNavigator.tsx index 94001b558f..60af288501 100644 --- a/MobileApp/src/navigation/IncidentEpisodesStackNavigator.tsx +++ b/MobileApp/src/navigation/IncidentEpisodesStackNavigator.tsx @@ -5,7 +5,7 @@ import IncidentEpisodesScreen from "../screens/IncidentEpisodesScreen"; import IncidentEpisodeDetailScreen from "../screens/IncidentEpisodeDetailScreen"; import type { IncidentEpisodesStackParamList } from "./types"; -const Stack = createNativeStackNavigator(); +const Stack: ReturnType> = createNativeStackNavigator(); export default function IncidentEpisodesStackNavigator(): React.JSX.Element { const { theme } = useTheme(); diff --git a/MobileApp/src/navigation/IncidentsStackNavigator.tsx b/MobileApp/src/navigation/IncidentsStackNavigator.tsx index 03f62a28ee..2abbbada0f 100644 --- a/MobileApp/src/navigation/IncidentsStackNavigator.tsx +++ b/MobileApp/src/navigation/IncidentsStackNavigator.tsx @@ -5,7 +5,7 @@ import IncidentsScreen from "../screens/IncidentsScreen"; import IncidentDetailScreen from "../screens/IncidentDetailScreen"; import type { IncidentsStackParamList } from "./types"; -const Stack = createNativeStackNavigator(); +const Stack: ReturnType> = createNativeStackNavigator(); export default function IncidentsStackNavigator(): React.JSX.Element { const { theme } = useTheme(); diff --git a/MobileApp/src/navigation/MainTabNavigator.tsx b/MobileApp/src/navigation/MainTabNavigator.tsx index be77e0c7dd..07f084c218 100644 --- a/MobileApp/src/navigation/MainTabNavigator.tsx +++ b/MobileApp/src/navigation/MainTabNavigator.tsx @@ -9,7 +9,8 @@ import AlertEpisodesStackNavigator from "./AlertEpisodesStackNavigator"; import SettingsStackNavigator from "./SettingsStackNavigator"; import { useTheme } from "../theme"; -const Tab: ReturnType> = createBottomTabNavigator(); +const Tab: ReturnType> = + createBottomTabNavigator(); export default function MainTabNavigator(): React.JSX.Element { const { theme } = useTheme(); diff --git a/MobileApp/src/navigation/RootNavigator.tsx b/MobileApp/src/navigation/RootNavigator.tsx index f3197d2f15..96ecf07abc 100644 --- a/MobileApp/src/navigation/RootNavigator.tsx +++ b/MobileApp/src/navigation/RootNavigator.tsx @@ -17,9 +17,9 @@ import ProjectSelectionScreen from "../screens/ProjectSelectionScreen"; import BiometricLockScreen from "../screens/BiometricLockScreen"; import { View, ActivityIndicator, StyleSheet } from "react-native"; -const prefix = Linking.createURL("/"); +const prefix: string = Linking.createURL("/"); -const linking = { +const linking: React.ComponentProps["linking"] = { prefixes: [prefix, "oneuptime://"], config: { screens: { @@ -52,8 +52,8 @@ export default function RootNavigator(): React.JSX.Element { const { theme } = useTheme(); const { isAuthenticated, isLoading, needsServerUrl } = useAuth(); const { selectedProject, isLoadingProjects } = useProject(); - const navigationRef = useNavigationContainerRef(); - const biometric = useBiometric(); + const navigationRef: ReturnType = useNavigationContainerRef(); + const biometric: ReturnType = useBiometric(); const [biometricPassed, setBiometricPassed] = useState(false); const [biometricChecked, setBiometricChecked] = useState(false); @@ -62,7 +62,7 @@ export default function RootNavigator(): React.JSX.Element { // Check biometric on app launch useEffect(() => { - const checkBiometric = async (): Promise => { + const checkBiometric: () => Promise = async (): Promise => { if (!isAuthenticated || !biometric.isEnabled) { setBiometricPassed(true); setBiometricChecked(true); @@ -106,7 +106,7 @@ export default function RootNavigator(): React.JSX.Element { ); } - const renderContent = (): React.JSX.Element => { + const renderContent: () => React.JSX.Element = (): React.JSX.Element => { if (!isAuthenticated) { return ( = StyleSheet.create({ loading: { flex: 1, alignItems: "center", diff --git a/MobileApp/src/navigation/SettingsStackNavigator.tsx b/MobileApp/src/navigation/SettingsStackNavigator.tsx index b75a4e9a77..c23805267b 100644 --- a/MobileApp/src/navigation/SettingsStackNavigator.tsx +++ b/MobileApp/src/navigation/SettingsStackNavigator.tsx @@ -5,7 +5,7 @@ import SettingsScreen from "../screens/SettingsScreen"; import NotificationPreferencesScreen from "../screens/NotificationPreferencesScreen"; import type { SettingsStackParamList } from "./types"; -const Stack = createNativeStackNavigator(); +const Stack: ReturnType> = createNativeStackNavigator(); export default function SettingsStackNavigator(): React.JSX.Element { const { theme } = useTheme(); diff --git a/MobileApp/src/notifications/handlers.ts b/MobileApp/src/notifications/handlers.ts index bbcf62b3d8..1425b6e5c7 100644 --- a/MobileApp/src/notifications/handlers.ts +++ b/MobileApp/src/notifications/handlers.ts @@ -52,9 +52,9 @@ function navigateToEntity(data: NotificationData): void { export function handleNotificationResponse( response: NotificationResponse, ): void { - const data = + const data: NotificationData = (response.notification.request.content.data as NotificationData) || {}; - const actionId = response.actionIdentifier; + const actionId: string = response.actionIdentifier; if (actionId === "ACKNOWLEDGE") { // Background acknowledge — could call API here in the future diff --git a/MobileApp/src/notifications/setup.ts b/MobileApp/src/notifications/setup.ts index 451c76b217..b2b0d2bf81 100644 --- a/MobileApp/src/notifications/setup.ts +++ b/MobileApp/src/notifications/setup.ts @@ -1,7 +1,9 @@ import * as Notifications from "expo-notifications"; +import type { ExpoPushToken } from "expo-notifications"; import * as Device from "expo-device"; import Constants from "expo-constants"; import { Platform } from "react-native"; +import { PermissionStatus } from "expo-modules-core"; // Show notifications when app is in foreground Notifications.setNotificationHandler({ @@ -82,7 +84,7 @@ export async function requestPermissionsAndGetToken(): Promise { } const { status: existingStatus } = await Notifications.getPermissionsAsync(); - let finalStatus = existingStatus; + let finalStatus: PermissionStatus = existingStatus; if (existingStatus !== "granted") { const { status } = await Notifications.requestPermissionsAsync(); @@ -93,7 +95,7 @@ export async function requestPermissionsAndGetToken(): Promise { return null; } - const projectId = + const projectId: string | undefined = Constants.expoConfig?.extra?.eas?.projectId ?? Constants.easConfig?.projectId; @@ -101,7 +103,7 @@ export async function requestPermissionsAndGetToken(): Promise { return null; } - const tokenData = await Notifications.getExpoPushTokenAsync({ + const tokenData: ExpoPushToken = await Notifications.getExpoPushTokenAsync({ projectId, }); diff --git a/MobileApp/src/screens/AlertDetailScreen.tsx b/MobileApp/src/screens/AlertDetailScreen.tsx index ea3ebe1140..965d994995 100644 --- a/MobileApp/src/screens/AlertDetailScreen.tsx +++ b/MobileApp/src/screens/AlertDetailScreen.tsx @@ -24,11 +24,7 @@ import { rgbToHex } from "../utils/color"; import { formatDateTime } from "../utils/date"; import type { AlertsStackParamList } from "../navigation/types"; import { QueryClient, useQueryClient } from "@tanstack/react-query"; -import type { - AlertState, - StateTimelineItem, - NoteItem, -} from "../api/types"; +import type { AlertState, StateTimelineItem, NoteItem } from "../api/types"; import AddNoteModal from "../components/AddNoteModal"; import SkeletonCard from "../components/SkeletonCard"; import { useHaptics } from "../hooks/useHaptics"; @@ -66,7 +62,10 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element { await Promise.all([refetchAlert(), refetchTimeline(), refetchNotes()]); }, [refetchAlert, refetchTimeline, refetchNotes]); - const handleStateChange: (stateId: string, stateName: string) => Promise = useCallback( + const handleStateChange: ( + stateId: string, + stateName: string, + ) => Promise = useCallback( async (stateId: string, stateName: string) => { if (!alert) { return; @@ -166,9 +165,11 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element { : theme.colors.textTertiary; // Find acknowledge and resolve states from fetched state definitions - const acknowledgeState: AlertState | undefined = states?.find((s: AlertState) => { - return s.isAcknowledgedState; - }); + const acknowledgeState: AlertState | undefined = states?.find( + (s: AlertState) => { + return s.isAcknowledgedState; + }, + ); const resolveState: AlertState | undefined = states?.find((s: AlertState) => { return s.isResolvedState; }); diff --git a/MobileApp/src/screens/AlertEpisodeDetailScreen.tsx b/MobileApp/src/screens/AlertEpisodeDetailScreen.tsx index 2094f5b49c..926c26905b 100644 --- a/MobileApp/src/screens/AlertEpisodeDetailScreen.tsx +++ b/MobileApp/src/screens/AlertEpisodeDetailScreen.tsx @@ -26,11 +26,7 @@ import { rgbToHex } from "../utils/color"; import { formatDateTime } from "../utils/date"; import type { AlertEpisodesStackParamList } from "../navigation/types"; import { QueryClient, useQueryClient } from "@tanstack/react-query"; -import type { - AlertState, - StateTimelineItem, - NoteItem, -} from "../api/types"; +import type { AlertState, StateTimelineItem, NoteItem } from "../api/types"; import AddNoteModal from "../components/AddNoteModal"; import SkeletonCard from "../components/SkeletonCard"; import { useHaptics } from "../hooks/useHaptics"; @@ -71,7 +67,10 @@ export default function AlertEpisodeDetailScreen({ await Promise.all([refetchEpisode(), refetchTimeline(), refetchNotes()]); }, [refetchEpisode, refetchTimeline, refetchNotes]); - const handleStateChange: (stateId: string, stateName: string) => Promise = useCallback( + const handleStateChange: ( + stateId: string, + stateName: string, + ) => Promise = useCallback( async (stateId: string, stateName: string) => { if (!episode) { return; @@ -172,9 +171,11 @@ export default function AlertEpisodeDetailScreen({ ? rgbToHex(episode.alertSeverity.color) : theme.colors.textTertiary; - const acknowledgeState: AlertState | undefined = states?.find((s: AlertState) => { - return s.isAcknowledgedState; - }); + const acknowledgeState: AlertState | undefined = states?.find( + (s: AlertState) => { + return s.isAcknowledgedState; + }, + ); const resolveState: AlertState | undefined = states?.find((s: AlertState) => { return s.isResolvedState; }); diff --git a/MobileApp/src/screens/AlertEpisodesScreen.tsx b/MobileApp/src/screens/AlertEpisodesScreen.tsx index 416ffc30db..81de5400db 100644 --- a/MobileApp/src/screens/AlertEpisodesScreen.tsx +++ b/MobileApp/src/screens/AlertEpisodesScreen.tsx @@ -6,6 +6,7 @@ import { TouchableOpacity, Text, StyleSheet, + ListRenderItemInfo, } from "react-native"; import { useNavigation } from "@react-navigation/native"; import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; @@ -139,7 +140,7 @@ export default function AlertEpisodesScreen(): React.JSX.Element { contentContainerStyle={ episodes.length === 0 ? styles.emptyContainer : styles.list } - renderItem={({ item }: { item: AlertEpisodeItem }) => { + renderItem={({ item }: ListRenderItemInfo) => { return ( { - return s.isAcknowledgedState; - }); + const acknowledgeState: AlertState | undefined = states?.find( + (s: AlertState) => { + return s.isAcknowledgedState; + }, + ); const alerts: AlertItem[] = data?.data ?? []; const totalCount: number = data?.count ?? 0; @@ -168,7 +171,7 @@ export default function AlertsScreen(): React.JSX.Element { contentContainerStyle={ alerts.length === 0 ? styles.emptyContainer : styles.list } - renderItem={({ item }: { item: AlertItem }) => { + renderItem={({ item }: ListRenderItemInfo) => { return ( => { - const result = await LocalAuthentication.authenticateAsync({ + const authenticate: () => Promise = async (): Promise => { + const result: LocalAuthentication.LocalAuthenticationResult = await LocalAuthentication.authenticateAsync({ promptMessage: "Unlock OneUptime", fallbackLabel: "Use passcode", disableDeviceFallback: false, @@ -100,7 +100,7 @@ export default function BiometricLockScreen({ ); } -const styles = StyleSheet.create({ +const styles: ReturnType = StyleSheet.create({ container: { flex: 1, alignItems: "center", diff --git a/MobileApp/src/screens/HomeScreen.tsx b/MobileApp/src/screens/HomeScreen.tsx index 5ea5a840db..77aa55b062 100644 --- a/MobileApp/src/screens/HomeScreen.tsx +++ b/MobileApp/src/screens/HomeScreen.tsx @@ -38,7 +38,7 @@ function StatCard({ const { theme } = useTheme(); const { lightImpact } = useHaptics(); - const handlePress = (): void => { + const handlePress: () => void = (): void => { lightImpact(); onPress(); }; @@ -101,8 +101,8 @@ function QuickLink({ label, onPress }: QuickLinkProps): React.JSX.Element { export default function HomeScreen(): React.JSX.Element { const { theme } = useTheme(); const { selectedProject } = useProject(); - const projectId = selectedProject?._id ?? ""; - const navigation = useNavigation(); + const projectId: string = selectedProject?._id ?? ""; + const navigation: HomeNavProp = useNavigation(); const { data: incidentCount, @@ -130,7 +130,7 @@ export default function HomeScreen(): React.JSX.Element { const { lightImpact } = useHaptics(); - const onRefresh = async (): Promise => { + const onRefresh: () => Promise = async (): Promise => { lightImpact(); await Promise.all([ refetchIncidents(), @@ -250,7 +250,7 @@ export default function HomeScreen(): React.JSX.Element { ); } -const styles = StyleSheet.create({ +const styles: ReturnType = StyleSheet.create({ content: { padding: 20, paddingBottom: 40, diff --git a/MobileApp/src/screens/IncidentEpisodeDetailScreen.tsx b/MobileApp/src/screens/IncidentEpisodeDetailScreen.tsx index 5fdce430e8..38856a72b8 100644 --- a/MobileApp/src/screens/IncidentEpisodeDetailScreen.tsx +++ b/MobileApp/src/screens/IncidentEpisodeDetailScreen.tsx @@ -25,6 +25,7 @@ import { import { rgbToHex } from "../utils/color"; import { formatDateTime } from "../utils/date"; import type { IncidentEpisodesStackParamList } from "../navigation/types"; +import type { IncidentState, StateTimelineItem, NoteItem } from "../api/types"; import { useQueryClient } from "@tanstack/react-query"; import AddNoteModal from "../components/AddNoteModal"; import SkeletonCard from "../components/SkeletonCard"; @@ -41,8 +42,8 @@ export default function IncidentEpisodeDetailScreen({ const { episodeId } = route.params; const { theme } = useTheme(); const { selectedProject } = useProject(); - const projectId = selectedProject?._id ?? ""; - const queryClient = useQueryClient(); + const projectId: string = selectedProject?._id ?? ""; + const queryClient: ReturnType = useQueryClient(); const { data: episode, @@ -62,18 +63,18 @@ export default function IncidentEpisodeDetailScreen({ const [noteModalVisible, setNoteModalVisible] = useState(false); const [submittingNote, setSubmittingNote] = useState(false); - const onRefresh = useCallback(async () => { + const onRefresh: () => Promise = useCallback(async (): Promise => { await Promise.all([refetchEpisode(), refetchTimeline(), refetchNotes()]); }, [refetchEpisode, refetchTimeline, refetchNotes]); - const handleStateChange = useCallback( - async (stateId: string, stateName: string) => { + const handleStateChange: (stateId: string, stateName: string) => Promise = useCallback( + async (stateId: string, stateName: string): Promise => { if (!episode) { return; } - const queryKey = ["incident-episode", projectId, episodeId]; - const previousData = queryClient.getQueryData(queryKey); - const newState = states?.find((s) => { + const queryKey: string[] = ["incident-episode", projectId, episodeId]; + const previousData: unknown = queryClient.getQueryData(queryKey); + const newState: IncidentState | undefined = states?.find((s: IncidentState) => { return s._id === stateId; }); if (newState) { @@ -113,8 +114,8 @@ export default function IncidentEpisodeDetailScreen({ ], ); - const handleAddNote = useCallback( - async (noteText: string) => { + const handleAddNote: (noteText: string) => Promise = useCallback( + async (noteText: string): Promise => { setSubmittingNote(true); try { await createIncidentEpisodeNote(projectId, episodeId, noteText); @@ -159,24 +160,24 @@ export default function IncidentEpisodeDetailScreen({ ); } - const stateColor = episode.currentIncidentState?.color + const stateColor: string = episode.currentIncidentState?.color ? rgbToHex(episode.currentIncidentState.color) : theme.colors.textTertiary; - const severityColor = episode.incidentSeverity?.color + const severityColor: string = episode.incidentSeverity?.color ? rgbToHex(episode.incidentSeverity.color) : theme.colors.textTertiary; - const acknowledgeState = states?.find((s) => { + const acknowledgeState: IncidentState | undefined = states?.find((s: IncidentState) => { return s.isAcknowledgedState; }); - const resolveState = states?.find((s) => { + const resolveState: IncidentState | undefined = states?.find((s: IncidentState) => { return s.isResolvedState; }); - const currentStateId = episode.currentIncidentState?._id; - const isResolved = resolveState?._id === currentStateId; - const isAcknowledged = acknowledgeState?._id === currentStateId; + const currentStateId: string | undefined = episode.currentIncidentState?._id; + const isResolved: boolean = resolveState?._id === currentStateId; + const isAcknowledged: boolean = acknowledgeState?._id === currentStateId; return ( State Timeline - {timeline.map((entry) => { - const entryColor = entry.incidentState?.color + {timeline.map((entry: StateTimelineItem) => { + const entryColor: string = entry.incidentState?.color ? rgbToHex(entry.incidentState.color) : theme.colors.textTertiary; return ( @@ -472,7 +473,7 @@ export default function IncidentEpisodeDetailScreen({ {notes && notes.length > 0 - ? notes.map((note) => { + ? notes.map((note: NoteItem) => { return ( = StyleSheet.create({ centered: { flex: 1, alignItems: "center", diff --git a/MobileApp/src/screens/IncidentEpisodesScreen.tsx b/MobileApp/src/screens/IncidentEpisodesScreen.tsx index a767f5f701..3229fb7384 100644 --- a/MobileApp/src/screens/IncidentEpisodesScreen.tsx +++ b/MobileApp/src/screens/IncidentEpisodesScreen.tsx @@ -19,7 +19,7 @@ import EmptyState from "../components/EmptyState"; import type { IncidentEpisodesStackParamList } from "../navigation/types"; import type { IncidentEpisodeItem } from "../api/types"; -const PAGE_SIZE = 20; +const PAGE_SIZE: number = 20; type NavProp = NativeStackNavigationProp< IncidentEpisodesStackParamList, @@ -29,12 +29,12 @@ type NavProp = NativeStackNavigationProp< export default function IncidentEpisodesScreen(): React.JSX.Element { const { theme } = useTheme(); const { selectedProject } = useProject(); - const projectId = selectedProject?._id ?? ""; - const navigation = useNavigation(); + const projectId: string = selectedProject?._id ?? ""; + const navigation: NavProp = useNavigation(); const { lightImpact } = useHaptics(); const [page, setPage] = useState(0); - const skip = page * PAGE_SIZE; + const skip: number = page * PAGE_SIZE; const { data, isLoading, isError, refetch } = useIncidentEpisodes( projectId, @@ -42,25 +42,25 @@ export default function IncidentEpisodesScreen(): React.JSX.Element { PAGE_SIZE, ); - const episodes = data?.data ?? []; - const totalCount = data?.count ?? 0; - const hasMore = skip + PAGE_SIZE < totalCount; + const episodes: IncidentEpisodeItem[] = data?.data ?? []; + const totalCount: number = data?.count ?? 0; + const hasMore: boolean = skip + PAGE_SIZE < totalCount; - const onRefresh = useCallback(async () => { + const onRefresh: () => Promise = useCallback(async (): Promise => { lightImpact(); setPage(0); await refetch(); }, [refetch, lightImpact]); - const loadMore = useCallback(() => { + const loadMore: () => void = useCallback((): void => { if (hasMore && !isLoading) { - setPage((prev) => { + setPage((prev: number) => { return prev + 1; }); } }, [hasMore, isLoading]); - const handlePress = useCallback( + const handlePress: (episode: IncidentEpisodeItem) => void = useCallback( (episode: IncidentEpisodeItem) => { navigation.navigate("IncidentEpisodeDetail", { episodeId: episode._id, @@ -133,13 +133,13 @@ export default function IncidentEpisodesScreen(): React.JSX.Element { > { + keyExtractor={(item: IncidentEpisodeItem) => { return item._id; }} contentContainerStyle={ episodes.length === 0 ? styles.emptyContainer : styles.list } - renderItem={({ item }) => { + renderItem={({ item }: { item: IncidentEpisodeItem }) => { return ( = StyleSheet.create({ container: { flex: 1, }, diff --git a/MobileApp/src/screens/IncidentsScreen.tsx b/MobileApp/src/screens/IncidentsScreen.tsx index 8a8830165d..d83eace486 100644 --- a/MobileApp/src/screens/IncidentsScreen.tsx +++ b/MobileApp/src/screens/IncidentsScreen.tsx @@ -6,6 +6,7 @@ import { TouchableOpacity, Text, StyleSheet, + ListRenderItemInfo, } from "react-native"; import { useNavigation } from "@react-navigation/native"; import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; @@ -181,7 +182,7 @@ export default function IncidentsScreen(): React.JSX.Element { contentContainerStyle={ incidents.length === 0 ? styles.emptyContainer : styles.list } - renderItem={({ item }: { item: IncidentItem }) => { + renderItem={({ item }: ListRenderItemInfo) => { return ( void = useCallback( + const updatePref: ( + key: keyof NotificationPreferences, + value: boolean, + ) => void = useCallback( (key: keyof NotificationPreferences, value: boolean) => { selectionFeedback(); const updated: NotificationPreferences = { ...prefs, [key]: value }; diff --git a/MobileApp/src/screens/ProjectSelectionScreen.tsx b/MobileApp/src/screens/ProjectSelectionScreen.tsx index e377bb2b52..d02064765e 100644 --- a/MobileApp/src/screens/ProjectSelectionScreen.tsx +++ b/MobileApp/src/screens/ProjectSelectionScreen.tsx @@ -16,7 +16,7 @@ export default function ProjectSelectionScreen(): React.JSX.Element { const { projectList, isLoadingProjects, selectProject, refreshProjects } = useProject(); - const handleSelect = async (project: ProjectItem): Promise => { + const handleSelect: (project: ProjectItem) => Promise = async (project: ProjectItem): Promise => { await selectProject(project); }; @@ -67,7 +67,7 @@ export default function ProjectSelectionScreen(): React.JSX.Element { }, ]} > - You don't have access to any projects. + {"You don't have access to any projects."} { + keyExtractor={(item: ProjectItem) => { return item._id; }} contentContainerStyle={styles.list} - renderItem={({ item }) => { + renderItem={({ item }: { item: ProjectItem }) => { return ( = StyleSheet.create({ container: { flex: 1, }, diff --git a/MobileApp/src/screens/SettingsScreen.tsx b/MobileApp/src/screens/SettingsScreen.tsx index 3bfeb2c624..0d31e04a1f 100644 --- a/MobileApp/src/screens/SettingsScreen.tsx +++ b/MobileApp/src/screens/SettingsScreen.tsx @@ -22,7 +22,7 @@ type SettingsNavProp = NativeStackNavigationProp< "SettingsList" >; -const APP_VERSION = "1.0.0"; +const APP_VERSION: string = "1.0.0"; interface SettingsRowProps { label: string; @@ -43,7 +43,7 @@ function SettingsRow({ }: SettingsRowProps): React.JSX.Element { const { theme } = useTheme(); - const content = ( + const content: React.JSX.Element = ( = useBiometric(); const { selectionFeedback } = useHaptics(); - const navigation = useNavigation(); + const navigation: SettingsNavProp = useNavigation(); const [serverUrl, setServerUrlState] = useState(""); useEffect(() => { getServerUrl().then(setServerUrlState); }, []); - const handleChangeProject = async (): Promise => { + const handleChangeProject: () => Promise = async (): Promise => { await clearProject(); }; diff --git a/MobileApp/src/screens/auth/ServerUrlScreen.tsx b/MobileApp/src/screens/auth/ServerUrlScreen.tsx index 4becba4544..1911508793 100644 --- a/MobileApp/src/screens/auth/ServerUrlScreen.tsx +++ b/MobileApp/src/screens/auth/ServerUrlScreen.tsx @@ -28,7 +28,8 @@ type ServerUrlNavigationProp = NativeStackNavigationProp< export default function ServerUrlScreen(): React.JSX.Element { const { theme } = useTheme(); const { setNeedsServerUrl } = useAuth(); - const navigation: ServerUrlNavigationProp = useNavigation(); + const navigation: ServerUrlNavigationProp = + useNavigation(); const [url, setUrl] = useState("https://oneuptime.com"); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); diff --git a/MobileApp/src/storage/keychain.ts b/MobileApp/src/storage/keychain.ts index 4b6421883d..62ed908711 100644 --- a/MobileApp/src/storage/keychain.ts +++ b/MobileApp/src/storage/keychain.ts @@ -23,9 +23,10 @@ export async function storeTokens(tokens: StoredTokens): Promise { } export async function getTokens(): Promise { - const credentials: false | Keychain.UserCredentials = await Keychain.getGenericPassword({ - service: SERVICE_NAME, - }); + const credentials: false | Keychain.UserCredentials = + await Keychain.getGenericPassword({ + service: SERVICE_NAME, + }); if (!credentials || typeof credentials === "boolean") { cachedAccessToken = null; diff --git a/MobileApp/src/storage/preferences.ts b/MobileApp/src/storage/preferences.ts index 678581745e..29be6758c4 100644 --- a/MobileApp/src/storage/preferences.ts +++ b/MobileApp/src/storage/preferences.ts @@ -40,7 +40,9 @@ export async function setThemeMode(mode: ThemeMode): Promise { } export async function getBiometricEnabled(): Promise { - const stored: string | null = await AsyncStorage.getItem(KEYS.BIOMETRIC_ENABLED); + const stored: string | null = await AsyncStorage.getItem( + KEYS.BIOMETRIC_ENABLED, + ); return stored === "true"; } @@ -49,7 +51,9 @@ export async function setBiometricEnabled(enabled: boolean): Promise { } export async function getNotificationPreferences(): Promise { - const stored: string | null = await AsyncStorage.getItem(KEYS.NOTIFICATION_PREFS); + const stored: string | null = await AsyncStorage.getItem( + KEYS.NOTIFICATION_PREFS, + ); if (stored) { try { return { ...DEFAULT_NOTIFICATION_PREFS, ...JSON.parse(stored) }; diff --git a/MobileApp/src/theme/ThemeContext.tsx b/MobileApp/src/theme/ThemeContext.tsx index 2cd8e36a29..4660fba535 100644 --- a/MobileApp/src/theme/ThemeContext.tsx +++ b/MobileApp/src/theme/ThemeContext.tsx @@ -31,7 +31,8 @@ interface ThemeContextValue { setThemeMode: (mode: ThemeMode) => void; } -const ThemeContext: React.Context = createContext(undefined); +const ThemeContext: React.Context = + createContext(undefined); interface ThemeProviderProps { children: ReactNode; @@ -40,7 +41,8 @@ interface ThemeProviderProps { export function ThemeProvider({ children, }: ThemeProviderProps): React.JSX.Element { - const systemColorScheme: "light" | "dark" | null | undefined = useColorScheme(); + const systemColorScheme: "light" | "dark" | null | undefined = + useColorScheme(); const [themeMode, setThemeModeState] = useState("dark"); // Load persisted theme on mount