refactor: enhance type safety and improve code readability across multiple files

- Updated hooks to return specific types using UseQueryResult for better type safety.
- Refactored various components to explicitly define return types for functions and callbacks.
- Improved type annotations for variables and function parameters in screens and hooks.
- Enhanced readability by restructuring code and ensuring consistent formatting.
- Added missing type imports and ensured proper usage of types from the API.
- Cleaned up unnecessary type assertions and improved overall code clarity.
This commit is contained in:
Nawaz Dhandala
2026-02-10 22:38:45 +00:00
parent 59b3fc0334
commit 5413e24bd4
57 changed files with 429 additions and 263 deletions

View File

@@ -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 => {
<ModelDelete
modelType={Project}
modelId={modelId}
modelAPI={AdminModelAPI}
onDeleteSuccess={() => {
Navigation.navigate(RouteMap[PageMap.PROJECTS] as Route);
}}

View File

@@ -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 => {
<div>
<CardModelDetail<Project>
name="Project"
modelAPI={AdminModelAPI}
cardProps={{
title: "Project",
description: "Project details",

View File

@@ -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 => {
<ModelDelete
modelType={User}
modelId={modelId}
modelAPI={AdminModelAPI}
onDeleteSuccess={() => {
Navigation.navigate(RouteMap[PageMap.USERS] as Route);
}}

View File

@@ -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 => {
<div>
<CardModelDetail<User>
name="User"
modelAPI={AdminModelAPI}
cardProps={{
title: "User",
description: "User details",

View File

@@ -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 => {
>
<CardModelDetail<User>
name="user-master-admin-settings"
modelAPI={AdminModelAPI}
cardProps={{
title: "Master Admin Access",
description:

View File

@@ -12,6 +12,7 @@ import React, { ReactElement, useState } from "react";
export interface ComponentProps<TBaseModel extends BaseModel> {
modelType: { new (): TBaseModel };
modelId: ObjectID;
modelAPI?: typeof ModelAPI | undefined;
onDeleteSuccess: () => void;
}
@@ -29,7 +30,9 @@ const ModelDelete: <TBaseModel extends BaseModel>(
const deleteItem: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
try {
await ModelAPI.deleteItem<TBaseModel>({
const modelAPI: typeof ModelAPI = props.modelAPI || ModelAPI;
await modelAPI.deleteItem<TBaseModel>({
modelType: props.modelType,
id: props.modelId,
});

View File

@@ -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<TBaseModel extends BaseModel> {
formFields?: undefined | Fields<TBaseModel>;
className?: string | undefined;
name: string;
modelAPI?: typeof ModelAPI | undefined;
createEditModalWidth?: ModalWidth | undefined;
refresher?: boolean;
createOrUpdateApiUrl?: URL | undefined;
@@ -130,6 +132,7 @@ const CardModelDetail: <TBaseModel extends BaseModel>(
<ModelDetail
refresher={refresher}
{...props.modelDetailProps}
modelAPI={props.modelAPI}
onItemLoaded={(item: TBaseModel) => {
setItem(item);
if (props.modelDetailProps.onItemLoaded) {
@@ -144,6 +147,7 @@ const CardModelDetail: <TBaseModel extends BaseModel>(
<ModelFormModal<TBaseModel>
title={`Edit ${model.singularName}`}
modalWidth={props.createEditModalWidth}
modelAPI={props.modelAPI}
onClose={() => {
setShowModal(false);
}}

View File

@@ -28,6 +28,7 @@ export interface ComponentProps<TBaseModel extends BaseModel> {
fields: Array<Field<TBaseModel>>;
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: <TBaseModel extends BaseModel>(
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: {

View File

@@ -20,6 +20,7 @@ export interface ComponentProps<TBaseModel extends BaseModel> {
modelType: { new (): TBaseModel };
modelId: ObjectID;
modelNameField: string;
modelAPI?: typeof ModelAPI | undefined;
}
const ModelPage: <TBaseModel extends BaseModel>(
@@ -54,7 +55,9 @@ const ModelPage: <TBaseModel extends BaseModel>(
} as Select<TBaseModel>;
}
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<TBaseModel>,

View File

@@ -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,
},

View File

@@ -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<typeof StyleSheet.create> = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.6)",

View File

@@ -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<typeof StyleSheet.create> = StyleSheet.create({
card: {
padding: 16,
borderRadius: 12,

View File

@@ -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<typeof StyleSheet.create> = StyleSheet.create({
container: {
position: "absolute",
top: 0,

View File

@@ -208,20 +208,22 @@ export default function SkeletonCard({
/>
</View>
{/* Body lines */}
{Array.from({ length: Math.max(lines - 1, 1) }).map((_: unknown, index: number) => {
return (
<View
key={index}
style={[
styles.line,
{
backgroundColor: theme.colors.backgroundTertiary,
width: lineWidths[index % lineWidths.length],
},
]}
/>
);
})}
{Array.from({ length: Math.max(lines - 1, 1) }).map(
(_: unknown, index: number) => {
return (
<View
key={index}
style={[
styles.line,
{
backgroundColor: theme.colors.backgroundTertiary,
width: lineWidths[index % lineWidths.length],
},
]}
/>
);
},
)}
</Animated.View>
);
}

View File

@@ -36,15 +36,21 @@ export default function SwipeableCard({
const translateX: Animated.Value = useRef(new Animated.Value(0)).current;
const hasTriggeredHaptic: React.MutableRefObject<boolean> = 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<typeof StyleSheet.create> = StyleSheet.create({
container: {
overflow: "hidden",
borderRadius: 12,

View File

@@ -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<AlertItem, Error> {
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<AlertState[], Error> {
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<StateTimelineItem[], Error> {
return useQuery({
queryKey: ["alert-state-timeline", projectId, alertId],
queryFn: () => {

View File

@@ -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<AlertEpisodeItem, Error> {
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<AlertState[], Error> {
return useQuery({
queryKey: ["alert-states", projectId],
queryFn: () => {
@@ -29,7 +40,7 @@ export function useAlertEpisodeStates(projectId: string) {
export function useAlertEpisodeStateTimeline(
projectId: string,
episodeId: string,
) {
): UseQueryResult<StateTimelineItem[], Error> {
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<NoteItem[], Error> {
return useQuery({
queryKey: ["alert-episode-notes", projectId, episodeId],
queryFn: () => {

View File

@@ -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<ListResponse<AlertEpisodeItem>, 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<number, Error> {
return useQuery({
queryKey: ["alert-episodes", "unresolved-count", projectId],
queryFn: async () => {
const response = await fetchAlertEpisodes(projectId, {
skip: 0,
limit: 1,
unresolvedOnly: true,
});
const response: ListResponse<AlertEpisodeItem> =
await fetchAlertEpisodes(projectId, {
skip: 0,
limit: 1,
unresolvedOnly: true,
});
return response.count;
},
enabled: Boolean(projectId),

View File

@@ -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<NoteItem[], Error> {
return useQuery({
queryKey: ["alert-notes", projectId, alertId],
queryFn: () => {

View File

@@ -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<ListResponse<AlertItem>, 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<number, Error> {
return useQuery({
queryKey: ["alerts", "unresolved-count", projectId],
queryFn: async () => {
const response = await fetchAlerts(projectId, {
const response: ListResponse<AlertItem> = await fetchAlerts(projectId, {
skip: 0,
limit: 1,
unresolvedOnly: true,

View File

@@ -27,7 +27,8 @@ interface AuthContextValue {
setIsAuthenticated: (value: boolean) => void;
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
const AuthContext: React.Context<AuthContextValue | undefined> =
createContext<AuthContextValue | undefined>(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<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [needsServerUrl, setNeedsServerUrl] = useState<boolean>(false);
const [user, setUser] = useState<LoginResponse["user"] | null>(null);
useEffect(() => {
useEffect((): void => {
const checkAuth = async (): Promise<void> => {
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<LoginResponse> => {
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");
}

View File

@@ -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<boolean>(false);
const [isEnabled, setIsEnabled] = useState<boolean>(false);
const [biometricType, setBiometricType] = useState<string>("Biometrics");
useEffect(() => {
useEffect((): void => {
const check = async (): Promise<void> => {
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<boolean> => {
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<void> => {
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,

View File

@@ -1,6 +1,14 @@
import * as Haptics from "expo-haptics";
export function useHaptics() {
interface HapticsResult {
successFeedback: () => Promise<void>;
errorFeedback: () => Promise<void>;
lightImpact: () => Promise<void>;
mediumImpact: () => Promise<void>;
selectionFeedback: () => Promise<void>;
}
export function useHaptics(): HapticsResult {
const successFeedback = async (): Promise<void> => {
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
};

View File

@@ -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<IncidentItem, Error> {
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<IncidentState[], Error> {
return useQuery({
queryKey: ["incident-states", projectId],
queryFn: () => {
@@ -28,7 +38,7 @@ export function useIncidentStates(projectId: string) {
export function useIncidentStateTimeline(
projectId: string,
incidentId: string,
) {
): UseQueryResult<StateTimelineItem[], Error> {
return useQuery({
queryKey: ["incident-state-timeline", projectId, incidentId],
queryFn: () => {

View File

@@ -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<IncidentEpisodeItem, Error> {
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<IncidentState[], Error> {
return useQuery({
queryKey: ["incident-states", projectId],
queryFn: () => {
@@ -29,7 +40,7 @@ export function useIncidentEpisodeStates(projectId: string) {
export function useIncidentEpisodeStateTimeline(
projectId: string,
episodeId: string,
) {
): UseQueryResult<StateTimelineItem[], Error> {
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<NoteItem[], Error> {
return useQuery({
queryKey: ["incident-episode-notes", projectId, episodeId],
queryFn: () => {

View File

@@ -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<ListResponse<IncidentEpisodeItem>, 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<number, Error> {
return useQuery({
queryKey: ["incident-episodes", "unresolved-count", projectId],
queryFn: async () => {
const response = await fetchIncidentEpisodes(projectId, {
skip: 0,
limit: 1,
unresolvedOnly: true,
});
const response: ListResponse<IncidentEpisodeItem> =
await fetchIncidentEpisodes(projectId, {
skip: 0,
limit: 1,
unresolvedOnly: true,
});
return response.count;
},
enabled: Boolean(projectId),

View File

@@ -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<NoteItem[], Error> {
return useQuery({
queryKey: ["incident-notes", projectId, incidentId],
queryFn: () => {

View File

@@ -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<ListResponse<IncidentItem>, 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<number, Error> {
return useQuery({
queryKey: ["incidents", "unresolved-count", projectId],
queryFn: async () => {
const response = await fetchIncidents(projectId, {
skip: 0,
limit: 1,
unresolvedOnly: true,
});
const response: ListResponse<IncidentItem> = await fetchIncidents(
projectId,
{
skip: 0,
limit: 1,
unresolvedOnly: true,
},
);
return response.count;
},
enabled: Boolean(projectId),

View File

@@ -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();
};
}, []);

View File

@@ -21,9 +21,8 @@ interface ProjectContextValue {
clearProject: () => Promise<void>;
}
const ProjectContext = createContext<ProjectContextValue | undefined>(
undefined,
);
const ProjectContext: React.Context<ProjectContextValue | undefined> =
createContext<ProjectContextValue | undefined>(undefined);
interface ProjectProviderProps {
children: ReactNode;
@@ -36,20 +35,23 @@ export function ProjectProvider({
null,
);
const [projectList, setProjectList] = useState<ProjectItem[]>([]);
const [isLoadingProjects, setIsLoadingProjects] = useState(true);
const [isLoadingProjects, setIsLoadingProjects] = useState<boolean>(true);
const loadProjects = useCallback(async (): Promise<void> => {
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");
}

View File

@@ -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<Subscription | null>(null);
const receivedListenerRef = useRef<Subscription | null>(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<void> => {
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<void> {
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);

View File

@@ -5,7 +5,7 @@ import AlertEpisodesScreen from "../screens/AlertEpisodesScreen";
import AlertEpisodeDetailScreen from "../screens/AlertEpisodeDetailScreen";
import type { AlertEpisodesStackParamList } from "./types";
const Stack = createNativeStackNavigator<AlertEpisodesStackParamList>();
const Stack: ReturnType<typeof createNativeStackNavigator<AlertEpisodesStackParamList>> = createNativeStackNavigator<AlertEpisodesStackParamList>();
export default function AlertEpisodesStackNavigator(): React.JSX.Element {
const { theme } = useTheme();

View File

@@ -5,7 +5,7 @@ import AlertsScreen from "../screens/AlertsScreen";
import AlertDetailScreen from "../screens/AlertDetailScreen";
import type { AlertsStackParamList } from "./types";
const Stack = createNativeStackNavigator<AlertsStackParamList>();
const Stack: ReturnType<typeof createNativeStackNavigator<AlertsStackParamList>> = createNativeStackNavigator<AlertsStackParamList>();
export default function AlertsStackNavigator(): React.JSX.Element {
const { theme } = useTheme();

View File

@@ -5,7 +5,7 @@ import ServerUrlScreen from "../screens/auth/ServerUrlScreen";
import LoginScreen from "../screens/auth/LoginScreen";
import { useTheme } from "../theme";
const Stack = createNativeStackNavigator<AuthStackParamList>();
const Stack: ReturnType<typeof createNativeStackNavigator<AuthStackParamList>> = createNativeStackNavigator<AuthStackParamList>();
interface AuthStackNavigatorProps {
initialRoute: keyof AuthStackParamList;

View File

@@ -5,7 +5,7 @@ import IncidentEpisodesScreen from "../screens/IncidentEpisodesScreen";
import IncidentEpisodeDetailScreen from "../screens/IncidentEpisodeDetailScreen";
import type { IncidentEpisodesStackParamList } from "./types";
const Stack = createNativeStackNavigator<IncidentEpisodesStackParamList>();
const Stack: ReturnType<typeof createNativeStackNavigator<IncidentEpisodesStackParamList>> = createNativeStackNavigator<IncidentEpisodesStackParamList>();
export default function IncidentEpisodesStackNavigator(): React.JSX.Element {
const { theme } = useTheme();

View File

@@ -5,7 +5,7 @@ import IncidentsScreen from "../screens/IncidentsScreen";
import IncidentDetailScreen from "../screens/IncidentDetailScreen";
import type { IncidentsStackParamList } from "./types";
const Stack = createNativeStackNavigator<IncidentsStackParamList>();
const Stack: ReturnType<typeof createNativeStackNavigator<IncidentsStackParamList>> = createNativeStackNavigator<IncidentsStackParamList>();
export default function IncidentsStackNavigator(): React.JSX.Element {
const { theme } = useTheme();

View File

@@ -9,7 +9,8 @@ import AlertEpisodesStackNavigator from "./AlertEpisodesStackNavigator";
import SettingsStackNavigator from "./SettingsStackNavigator";
import { useTheme } from "../theme";
const Tab: ReturnType<typeof createBottomTabNavigator<MainTabParamList>> = createBottomTabNavigator<MainTabParamList>();
const Tab: ReturnType<typeof createBottomTabNavigator<MainTabParamList>> =
createBottomTabNavigator<MainTabParamList>();
export default function MainTabNavigator(): React.JSX.Element {
const { theme } = useTheme();

View File

@@ -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<typeof NavigationContainer>["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<typeof useNavigationContainerRef> = useNavigationContainerRef();
const biometric: ReturnType<typeof useBiometric> = 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<void> => {
const checkBiometric: () => Promise<void> = async (): Promise<void> => {
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 (
<AuthStackNavigator
@@ -158,7 +158,7 @@ export default function RootNavigator(): React.JSX.Element {
);
}
const styles = StyleSheet.create({
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
loading: {
flex: 1,
alignItems: "center",

View File

@@ -5,7 +5,7 @@ import SettingsScreen from "../screens/SettingsScreen";
import NotificationPreferencesScreen from "../screens/NotificationPreferencesScreen";
import type { SettingsStackParamList } from "./types";
const Stack = createNativeStackNavigator<SettingsStackParamList>();
const Stack: ReturnType<typeof createNativeStackNavigator<SettingsStackParamList>> = createNativeStackNavigator<SettingsStackParamList>();
export default function SettingsStackNavigator(): React.JSX.Element {
const { theme } = useTheme();

View File

@@ -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

View File

@@ -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<string | null> {
}
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<string | null> {
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<string | null> {
return null;
}
const tokenData = await Notifications.getExpoPushTokenAsync({
const tokenData: ExpoPushToken = await Notifications.getExpoPushTokenAsync({
projectId,
});

View File

@@ -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<void> = useCallback(
const handleStateChange: (
stateId: string,
stateName: string,
) => Promise<void> = 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;
});

View File

@@ -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<void> = useCallback(
const handleStateChange: (
stateId: string,
stateName: string,
) => Promise<void> = 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;
});

View File

@@ -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<AlertEpisodeItem>) => {
return (
<EpisodeCard
episode={item}

View File

@@ -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";
@@ -45,9 +46,11 @@ export default function AlertsScreen(): React.JSX.Element {
const { successFeedback, errorFeedback, lightImpact } = useHaptics();
const queryClient: QueryClient = useQueryClient();
const acknowledgeState: AlertState | undefined = states?.find((s: AlertState) => {
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<AlertItem>) => {
return (
<SwipeableCard
rightAction={

View File

@@ -14,8 +14,8 @@ export default function BiometricLockScreen({
}: BiometricLockScreenProps): React.JSX.Element {
const { theme } = useTheme();
const authenticate = async (): Promise<void> => {
const result = await LocalAuthentication.authenticateAsync({
const authenticate: () => Promise<void> = async (): Promise<void> => {
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<typeof StyleSheet.create> = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",

View File

@@ -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<HomeNavProp>();
const projectId: string = selectedProject?._id ?? "";
const navigation: HomeNavProp = useNavigation<HomeNavProp>();
const {
data: incidentCount,
@@ -130,7 +130,7 @@ export default function HomeScreen(): React.JSX.Element {
const { lightImpact } = useHaptics();
const onRefresh = async (): Promise<void> => {
const onRefresh: () => Promise<void> = async (): Promise<void> => {
lightImpact();
await Promise.all([
refetchIncidents(),
@@ -250,7 +250,7 @@ export default function HomeScreen(): React.JSX.Element {
);
}
const styles = StyleSheet.create({
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
content: {
padding: 20,
paddingBottom: 40,

View File

@@ -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<typeof useQueryClient> = 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<void> = useCallback(async (): Promise<void> => {
await Promise.all([refetchEpisode(), refetchTimeline(), refetchNotes()]);
}, [refetchEpisode, refetchTimeline, refetchNotes]);
const handleStateChange = useCallback(
async (stateId: string, stateName: string) => {
const handleStateChange: (stateId: string, stateName: string) => Promise<void> = useCallback(
async (stateId: string, stateName: string): Promise<void> => {
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<void> = useCallback(
async (noteText: string): Promise<void> => {
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 (
<ScrollView
@@ -395,8 +396,8 @@ export default function IncidentEpisodeDetailScreen({
>
State Timeline
</Text>
{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({
</View>
{notes && notes.length > 0
? notes.map((note) => {
? notes.map((note: NoteItem) => {
return (
<View
key={note._id}
@@ -541,7 +542,7 @@ export default function IncidentEpisodeDetailScreen({
);
}
const styles = StyleSheet.create({
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
centered: {
flex: 1,
alignItems: "center",

View File

@@ -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<NavProp>();
const projectId: string = selectedProject?._id ?? "";
const navigation: NavProp = useNavigation<NavProp>();
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<void> = useCallback(async (): Promise<void> => {
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 {
>
<FlatList
data={episodes}
keyExtractor={(item) => {
keyExtractor={(item: IncidentEpisodeItem) => {
return item._id;
}}
contentContainerStyle={
episodes.length === 0 ? styles.emptyContainer : styles.list
}
renderItem={({ item }) => {
renderItem={({ item }: { item: IncidentEpisodeItem }) => {
return (
<EpisodeCard
episode={item}
@@ -167,7 +167,7 @@ export default function IncidentEpisodesScreen(): React.JSX.Element {
);
}
const styles = StyleSheet.create({
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
container: {
flex: 1,
},

View File

@@ -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<IncidentItem>) => {
return (
<SwipeableCard
rightAction={

View File

@@ -1,5 +1,13 @@
import React, { useState, useEffect, useCallback } from "react";
import { View, Text, ScrollView, Switch, StyleSheet, ViewStyle, TextStyle } from "react-native";
import {
View,
Text,
ScrollView,
Switch,
StyleSheet,
ViewStyle,
TextStyle,
} from "react-native";
import { useTheme } from "../theme";
import { useHaptics } from "../hooks/useHaptics";
import {
@@ -78,7 +86,10 @@ export default function NotificationPreferencesScreen(): React.JSX.Element {
});
}, []);
const updatePref: (key: keyof NotificationPreferences, value: boolean) => void = useCallback(
const updatePref: (
key: keyof NotificationPreferences,
value: boolean,
) => void = useCallback(
(key: keyof NotificationPreferences, value: boolean) => {
selectionFeedback();
const updated: NotificationPreferences = { ...prefs, [key]: value };

View File

@@ -16,7 +16,7 @@ export default function ProjectSelectionScreen(): React.JSX.Element {
const { projectList, isLoadingProjects, selectProject, refreshProjects } =
useProject();
const handleSelect = async (project: ProjectItem): Promise<void> => {
const handleSelect: (project: ProjectItem) => Promise<void> = async (project: ProjectItem): Promise<void> => {
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."}
</Text>
<TouchableOpacity
style={[
@@ -117,11 +117,11 @@ export default function ProjectSelectionScreen(): React.JSX.Element {
<FlatList
data={projectList}
keyExtractor={(item) => {
keyExtractor={(item: ProjectItem) => {
return item._id;
}}
contentContainerStyle={styles.list}
renderItem={({ item }) => {
renderItem={({ item }: { item: ProjectItem }) => {
return (
<TouchableOpacity
style={[
@@ -170,7 +170,7 @@ export default function ProjectSelectionScreen(): React.JSX.Element {
);
}
const styles = StyleSheet.create({
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
container: {
flex: 1,
},

View File

@@ -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 = (
<View
style={[
styles.row,
@@ -95,16 +95,16 @@ export default function SettingsScreen(): React.JSX.Element {
const { theme, themeMode, setThemeMode } = useTheme();
const { logout } = useAuth();
const { selectedProject, clearProject } = useProject();
const biometric = useBiometric();
const biometric: ReturnType<typeof useBiometric> = useBiometric();
const { selectionFeedback } = useHaptics();
const navigation = useNavigation<SettingsNavProp>();
const navigation: SettingsNavProp = useNavigation<SettingsNavProp>();
const [serverUrl, setServerUrlState] = useState("");
useEffect(() => {
getServerUrl().then(setServerUrlState);
}, []);
const handleChangeProject = async (): Promise<void> => {
const handleChangeProject: () => Promise<void> = async (): Promise<void> => {
await clearProject();
};

View File

@@ -28,7 +28,8 @@ type ServerUrlNavigationProp = NativeStackNavigationProp<
export default function ServerUrlScreen(): React.JSX.Element {
const { theme } = useTheme();
const { setNeedsServerUrl } = useAuth();
const navigation: ServerUrlNavigationProp = useNavigation<ServerUrlNavigationProp>();
const navigation: ServerUrlNavigationProp =
useNavigation<ServerUrlNavigationProp>();
const [url, setUrl] = useState("https://oneuptime.com");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

View File

@@ -23,9 +23,10 @@ export async function storeTokens(tokens: StoredTokens): Promise<void> {
}
export async function getTokens(): Promise<StoredTokens | null> {
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;

View File

@@ -40,7 +40,9 @@ export async function setThemeMode(mode: ThemeMode): Promise<void> {
}
export async function getBiometricEnabled(): Promise<boolean> {
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<void> {
}
export async function getNotificationPreferences(): Promise<NotificationPreferences> {
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) };

View File

@@ -31,7 +31,8 @@ interface ThemeContextValue {
setThemeMode: (mode: ThemeMode) => void;
}
const ThemeContext: React.Context<ThemeContextValue | undefined> = createContext<ThemeContextValue | undefined>(undefined);
const ThemeContext: React.Context<ThemeContextValue | undefined> =
createContext<ThemeContextValue | undefined>(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<ThemeMode>("dark");
// Load persisted theme on mount