From 59b3fc033409c3f2bee472e9505f0e17823dce7c Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Tue, 10 Feb 2026 22:29:37 +0000 Subject: [PATCH] Refactor screens and components for improved readability and consistency - Simplified state management and data fetching in IncidentEpisodeDetailScreen. - Enhanced code clarity by using arrow functions consistently and removing unnecessary destructuring. - Improved type annotations across various screens for better TypeScript support. - Streamlined the rendering of components in IncidentEpisodesScreen and IncidentsScreen. - Updated NotificationPreferencesScreen to use consistent function signatures and improved readability. - Refactored ProjectSelectionScreen and SettingsScreen for better structure and clarity. - Enhanced LoginScreen and ServerUrlScreen with clearer type definitions and improved error handling. - Updated storage utilities to ensure consistent type usage and improved code clarity. - Refactored theme context and spacing utilities for better type safety and readability. - Improved color and date utility functions for better maintainability. --- Common/Server/EnvironmentConfig.ts | 1 - Common/Server/Middleware/UserAuthorization.ts | 6 +- .../Services/UserNotificationRuleService.ts | 9 +- MobileApp/index.ts | 12 +- MobileApp/src/App.tsx | 7 +- MobileApp/src/api/alerts.ts | 9 +- MobileApp/src/api/auth.ts | 21 +- MobileApp/src/api/client.ts | 35 ++- MobileApp/src/api/incidents.ts | 9 +- MobileApp/src/api/pushDevice.ts | 10 +- MobileApp/src/components/AlertCard.tsx | 24 +- MobileApp/src/components/EmptyState.tsx | 21 +- MobileApp/src/components/EpisodeCard.tsx | 9 +- MobileApp/src/components/IncidentCard.tsx | 34 ++- MobileApp/src/components/ProjectBadge.tsx | 4 +- MobileApp/src/components/SeverityBadge.tsx | 13 +- MobileApp/src/components/SkeletonCard.tsx | 75 +++--- MobileApp/src/components/StateBadge.tsx | 13 +- MobileApp/src/components/SwipeableCard.tsx | 19 +- MobileApp/src/hooks/useAlertDetail.ts | 18 +- MobileApp/src/hooks/useAlertEpisodeDetail.ts | 34 +-- MobileApp/src/hooks/useAlertEpisodes.ts | 8 +- MobileApp/src/hooks/useAlertNotes.ts | 6 +- MobileApp/src/hooks/useAlerts.ts | 8 +- MobileApp/src/hooks/useAuth.tsx | 6 +- MobileApp/src/hooks/useIncidentDetail.ts | 18 +- .../src/hooks/useIncidentEpisodeDetail.ts | 34 +-- MobileApp/src/hooks/useIncidentEpisodes.ts | 8 +- MobileApp/src/hooks/useIncidentNotes.ts | 6 +- MobileApp/src/hooks/useIncidents.ts | 8 +- MobileApp/src/hooks/useProject.tsx | 10 +- MobileApp/src/hooks/usePushNotifications.ts | 11 +- MobileApp/src/navigation/MainTabNavigator.tsx | 2 +- MobileApp/src/navigation/RootNavigator.tsx | 4 +- MobileApp/src/notifications/setup.ts | 22 +- MobileApp/src/screens/AlertDetailScreen.tsx | 206 +++++++++------- .../src/screens/AlertEpisodeDetailScreen.tsx | 191 +++++++-------- MobileApp/src/screens/AlertEpisodesScreen.tsx | 52 ++-- MobileApp/src/screens/AlertsScreen.tsx | 96 +++++--- MobileApp/src/screens/BiometricLockScreen.tsx | 11 +- MobileApp/src/screens/HomeScreen.tsx | 46 ++-- .../src/screens/IncidentDetailScreen.tsx | 231 ++++++++++-------- .../screens/IncidentEpisodeDetailScreen.tsx | 154 ++++++------ .../src/screens/IncidentEpisodesScreen.tsx | 30 ++- MobileApp/src/screens/IncidentsScreen.tsx | 143 +++++++---- .../screens/NotificationPreferencesScreen.tsx | 62 +++-- .../src/screens/ProjectSelectionScreen.tsx | 75 +++--- MobileApp/src/screens/SettingsScreen.tsx | 40 ++- MobileApp/src/screens/auth/LoginScreen.tsx | 28 ++- .../src/screens/auth/ServerUrlScreen.tsx | 22 +- MobileApp/src/storage/keychain.ts | 12 +- MobileApp/src/storage/preferences.ts | 12 +- MobileApp/src/storage/serverUrl.ts | 8 +- MobileApp/src/theme/ThemeContext.tsx | 25 +- MobileApp/src/theme/spacing.ts | 14 +- MobileApp/src/utils/color.ts | 12 +- MobileApp/src/utils/date.ts | 18 +- 57 files changed, 1122 insertions(+), 900 deletions(-) diff --git a/Common/Server/EnvironmentConfig.ts b/Common/Server/EnvironmentConfig.ts index b0d72bb2e3..91dc48b530 100644 --- a/Common/Server/EnvironmentConfig.ts +++ b/Common/Server/EnvironmentConfig.ts @@ -547,4 +547,3 @@ export const InboundEmailDomain: string | undefined = export const InboundEmailWebhookSecret: string | undefined = process.env["INBOUND_EMAIL_WEBHOOK_SECRET"] || undefined; - diff --git a/Common/Server/Middleware/UserAuthorization.ts b/Common/Server/Middleware/UserAuthorization.ts index 2b3ce07627..3ffbc716e1 100644 --- a/Common/Server/Middleware/UserAuthorization.ts +++ b/Common/Server/Middleware/UserAuthorization.ts @@ -73,9 +73,9 @@ export default class UserMiddleware { } // 2. Fallback: Check Authorization: Bearer header (mobile app flow) - const authHeader: string | undefined = req.headers[ - "authorization" - ] as string | undefined; + const authHeader: string | undefined = req.headers["authorization"] as + | string + | undefined; if (authHeader && authHeader.startsWith("Bearer ")) { return authHeader.substring(7); } diff --git a/Common/Server/Services/UserNotificationRuleService.ts b/Common/Server/Services/UserNotificationRuleService.ts index 4230b4e739..d9a0bc6e1e 100644 --- a/Common/Server/Services/UserNotificationRuleService.ts +++ b/Common/Server/Services/UserNotificationRuleService.ts @@ -1118,7 +1118,8 @@ export class Service extends DatabaseService { }, ], message: pushMessage, - deviceType: notificationRuleItem.userPush.deviceType! as PushDeviceType, + deviceType: notificationRuleItem.userPush + .deviceType! as PushDeviceType, }, { projectId: options.projectId, @@ -1194,7 +1195,8 @@ export class Service extends DatabaseService { }, ], message: pushMessage, - deviceType: notificationRuleItem.userPush.deviceType! as PushDeviceType, + deviceType: notificationRuleItem.userPush + .deviceType! as PushDeviceType, }, { projectId: options.projectId, @@ -1271,7 +1273,8 @@ export class Service extends DatabaseService { }, ], message: pushMessage, - deviceType: notificationRuleItem.userPush.deviceType! as PushDeviceType, + deviceType: notificationRuleItem.userPush + .deviceType! as PushDeviceType, }, { projectId: options.projectId, diff --git a/MobileApp/index.ts b/MobileApp/index.ts index 1d6e981ef6..8e3611a9a1 100644 --- a/MobileApp/index.ts +++ b/MobileApp/index.ts @@ -1,8 +1,10 @@ -import { registerRootComponent } from 'expo'; +import { registerRootComponent } from "expo"; -import App from './App'; +import App from "./App"; -// registerRootComponent calls AppRegistry.registerComponent('main', () => App); -// It also ensures that whether you load the app in Expo Go or in a native build, -// the environment is set up appropriately +/* + * registerRootComponent calls AppRegistry.registerComponent('main', () => App); + * It also ensures that whether you load the app in Expo Go or in a native build, + * the environment is set up appropriately + */ registerRootComponent(App); diff --git a/MobileApp/src/App.tsx b/MobileApp/src/App.tsx index 0a9e9056aa..6745c42f69 100644 --- a/MobileApp/src/App.tsx +++ b/MobileApp/src/App.tsx @@ -28,7 +28,12 @@ function AppContent(): React.JSX.Element { const { theme } = useTheme(); return ( - + diff --git a/MobileApp/src/api/alerts.ts b/MobileApp/src/api/alerts.ts index be3b0b0855..fca8a7ce73 100644 --- a/MobileApp/src/api/alerts.ts +++ b/MobileApp/src/api/alerts.ts @@ -1,3 +1,4 @@ +import type { AxiosResponse } from "axios"; import apiClient from "./client"; import type { ListResponse, @@ -17,7 +18,7 @@ export async function fetchAlerts( query.currentAlertState = { isResolvedState: false }; } - const response = await apiClient.post( + const response: AxiosResponse = await apiClient.post( `/api/alert/get-list?skip=${skip}&limit=${limit}`, { query, @@ -45,7 +46,7 @@ export async function fetchAlertById( projectId: string, alertId: string, ): Promise { - const response = await apiClient.post( + const response: AxiosResponse = await apiClient.post( "/api/alert/get-list?skip=0&limit=1", { query: { _id: alertId }, @@ -72,7 +73,7 @@ export async function fetchAlertById( export async function fetchAlertStates( projectId: string, ): Promise { - const response = await apiClient.post( + const response: AxiosResponse = await apiClient.post( "/api/alert-state/get-list?skip=0&limit=20", { query: {}, @@ -98,7 +99,7 @@ export async function fetchAlertStateTimeline( projectId: string, alertId: string, ): Promise { - const response = await apiClient.post( + const response: AxiosResponse = await apiClient.post( "/api/alert-state-timeline/get-list?skip=0&limit=50", { query: { alertId }, diff --git a/MobileApp/src/api/auth.ts b/MobileApp/src/api/auth.ts index 24a7382f3e..17d7b17766 100644 --- a/MobileApp/src/api/auth.ts +++ b/MobileApp/src/api/auth.ts @@ -1,7 +1,11 @@ -import axios from "axios"; +import axios, { AxiosResponse } from "axios"; import apiClient from "./client"; import { getServerUrl } from "../storage/serverUrl"; -import { storeTokens, clearTokens } from "../storage/keychain"; +import { + storeTokens, + clearTokens, + type StoredTokens, +} from "../storage/keychain"; export interface LoginResponse { accessToken: string; @@ -18,7 +22,7 @@ export interface LoginResponse { export async function validateServerUrl(url: string): Promise { try { - const response = await axios.get(`${url}/api/status`, { + const response: AxiosResponse = await axios.get(`${url}/api/status`, { timeout: 10000, }); return response.status === 200; @@ -31,9 +35,9 @@ export async function login( email: string, password: string, ): Promise { - const serverUrl = await getServerUrl(); + const serverUrl: string = await getServerUrl(); - const response = await apiClient.post( + const response: AxiosResponse = await apiClient.post( `${serverUrl}/identity/login`, { data: { @@ -53,7 +57,8 @@ export async function login( }, ); - const responseData = response.data; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const responseData: any = response.data; // Check if 2FA is required if ( @@ -91,9 +96,9 @@ export async function login( export async function logout(): Promise { try { - const serverUrl = await getServerUrl(); + const serverUrl: string = await getServerUrl(); const { getTokens } = await import("../storage/keychain"); - const tokens = await getTokens(); + const tokens: StoredTokens | null = await getTokens(); if (tokens?.refreshToken) { await apiClient.post( diff --git a/MobileApp/src/api/client.ts b/MobileApp/src/api/client.ts index 069193399d..bfbf718fe4 100644 --- a/MobileApp/src/api/client.ts +++ b/MobileApp/src/api/client.ts @@ -1,13 +1,19 @@ -import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosError } from "axios"; +import axios, { + AxiosInstance, + AxiosResponse, + InternalAxiosRequestConfig, + AxiosError, +} from "axios"; import { getServerUrl } from "../storage/serverUrl"; import { getCachedAccessToken, getTokens, storeTokens, clearTokens, + type StoredTokens, } from "../storage/keychain"; -let isRefreshing = false; +let isRefreshing: boolean = false; let refreshSubscribers: Array<(token: string) => void> = []; let onAuthFailure: (() => void) | null = null; @@ -16,7 +22,7 @@ function subscribeTokenRefresh(callback: (token: string) => void): void { } function onTokenRefreshed(newToken: string): void { - refreshSubscribers.forEach((callback) => { + refreshSubscribers.forEach((callback: (token: string) => void) => { callback(newToken); }); refreshSubscribers = []; @@ -40,7 +46,7 @@ apiClient.interceptors.request.use( config.baseURL = await getServerUrl(); } - const token = getCachedAccessToken(); + const token: string | null = getCachedAccessToken(); if (token && config.headers) { config.headers.Authorization = `Bearer ${token}`; } @@ -51,11 +57,13 @@ apiClient.interceptors.request.use( // Response interceptor: handle 401 with token refresh queue apiClient.interceptors.response.use( - (response) => { + (response: AxiosResponse) => { return response; }, async (error: AxiosError) => { - const originalRequest = error.config as InternalAxiosRequestConfig & { + const originalRequest: InternalAxiosRequestConfig & { + _retry?: boolean; + } = error.config as InternalAxiosRequestConfig & { _retry?: boolean; }; @@ -64,7 +72,7 @@ apiClient.interceptors.response.use( } if (isRefreshing) { - return new Promise((resolve) => { + return new Promise((resolve: (value: AxiosResponse) => void) => { subscribeTokenRefresh((newToken: string) => { if (originalRequest.headers) { originalRequest.headers.Authorization = `Bearer ${newToken}`; @@ -78,15 +86,18 @@ apiClient.interceptors.response.use( isRefreshing = true; try { - const tokens = await getTokens(); + const tokens: StoredTokens | null = await getTokens(); if (!tokens?.refreshToken) { throw new Error("No refresh token available"); } - const serverUrl = await getServerUrl(); - const response = await axios.post(`${serverUrl}/identity/refresh-token`, { - refreshToken: tokens.refreshToken, - }); + const serverUrl: string = await getServerUrl(); + const response: AxiosResponse = await axios.post( + `${serverUrl}/identity/refresh-token`, + { + refreshToken: tokens.refreshToken, + }, + ); const { accessToken, refreshToken, refreshTokenExpiresAt } = response.data; diff --git a/MobileApp/src/api/incidents.ts b/MobileApp/src/api/incidents.ts index c8cee72481..3540e606b6 100644 --- a/MobileApp/src/api/incidents.ts +++ b/MobileApp/src/api/incidents.ts @@ -1,3 +1,4 @@ +import type { AxiosResponse } from "axios"; import apiClient from "./client"; import type { ListResponse, @@ -17,7 +18,7 @@ export async function fetchIncidents( query.currentIncidentState = { isResolvedState: false }; } - const response = await apiClient.post( + const response: AxiosResponse = await apiClient.post( `/api/incident/get-list?skip=${skip}&limit=${limit}`, { query, @@ -46,7 +47,7 @@ export async function fetchIncidentById( projectId: string, incidentId: string, ): Promise { - const response = await apiClient.post( + const response: AxiosResponse = await apiClient.post( "/api/incident/get-list?skip=0&limit=1", { query: { _id: incidentId }, @@ -74,7 +75,7 @@ export async function fetchIncidentById( export async function fetchIncidentStates( projectId: string, ): Promise { - const response = await apiClient.post( + const response: AxiosResponse = await apiClient.post( "/api/incident-state/get-list?skip=0&limit=20", { query: {}, @@ -100,7 +101,7 @@ export async function fetchIncidentStateTimeline( projectId: string, incidentId: string, ): Promise { - const response = await apiClient.post( + const response: AxiosResponse = await apiClient.post( "/api/incident-state-timeline/get-list?skip=0&limit=50", { query: { incidentId }, diff --git a/MobileApp/src/api/pushDevice.ts b/MobileApp/src/api/pushDevice.ts index 9664650936..cfa28ceea5 100644 --- a/MobileApp/src/api/pushDevice.ts +++ b/MobileApp/src/api/pushDevice.ts @@ -7,7 +7,11 @@ export async function registerPushDevice(params: { projectId: string; }): Promise { const deviceType = - Platform.OS === "ios" ? "iOS" : Platform.OS === "android" ? "Android" : "Web"; + Platform.OS === "ios" + ? "iOS" + : Platform.OS === "android" + ? "Android" + : "Web"; try { await apiClient.post("/api/user-push/register", { @@ -25,9 +29,7 @@ export async function registerPushDevice(params: { } } -export async function unregisterPushDevice( - deviceToken: string, -): Promise { +export async function unregisterPushDevice(deviceToken: string): Promise { try { await apiClient.post("/api/user-push/unregister", { deviceToken: deviceToken, diff --git a/MobileApp/src/components/AlertCard.tsx b/MobileApp/src/components/AlertCard.tsx index 5b4e5d9b4e..eadd873e57 100644 --- a/MobileApp/src/components/AlertCard.tsx +++ b/MobileApp/src/components/AlertCard.tsx @@ -16,15 +16,15 @@ export default function AlertCard({ }: AlertCardProps): React.JSX.Element { const { theme } = useTheme(); - const stateColor = alert.currentAlertState?.color + const stateColor: string = alert.currentAlertState?.color ? rgbToHex(alert.currentAlertState.color) : theme.colors.textTertiary; - const severityColor = alert.alertSeverity?.color + const severityColor: string = alert.alertSeverity?.color ? rgbToHex(alert.alertSeverity.color) : theme.colors.textTertiary; - const timeString = formatRelativeTime(alert.createdAt); + const timeString: string = formatRelativeTime(alert.createdAt); return ( - + {alert.alertNumberWithPrefix || `#${alert.alertNumber}`} @@ -73,7 +68,9 @@ export default function AlertCard({ ]} > - + {alert.currentAlertState.name} @@ -81,10 +78,7 @@ export default function AlertCard({ {alert.alertSeverity ? ( {alert.alertSeverity.name} @@ -105,7 +99,7 @@ export default function AlertCard({ ); } -const styles = StyleSheet.create({ +const styles: ReturnType = StyleSheet.create({ card: { padding: 16, borderRadius: 12, diff --git a/MobileApp/src/components/EmptyState.tsx b/MobileApp/src/components/EmptyState.tsx index 04fdd57108..00f3990e9c 100644 --- a/MobileApp/src/components/EmptyState.tsx +++ b/MobileApp/src/components/EmptyState.tsx @@ -17,17 +17,14 @@ function EmptyIcon({ icon: EmptyIcon; color: string; }): React.JSX.Element { - // Simple geometric SVG-style icons using View primitives - // Monochrome, clean, professional — not cartoon/playful + /* + * Simple geometric SVG-style icons using View primitives + * Monochrome, clean, professional — not cartoon/playful + */ if (icon === "incidents") { return ( - + @@ -78,7 +75,11 @@ export default function EmptyState({ {title} @@ -102,7 +103,7 @@ export default function EmptyState({ ); } -const styles = StyleSheet.create({ +const styles: ReturnType = StyleSheet.create({ container: { flex: 1, alignItems: "center", diff --git a/MobileApp/src/components/EpisodeCard.tsx b/MobileApp/src/components/EpisodeCard.tsx index 3f6c8cb2fe..dab45d69aa 100644 --- a/MobileApp/src/components/EpisodeCard.tsx +++ b/MobileApp/src/components/EpisodeCard.tsx @@ -63,9 +63,7 @@ export default function EpisodeCard( activeOpacity={0.7} > - + {episode.episodeNumberWithPrefix || `#${episode.episodeNumber}`} @@ -102,10 +100,7 @@ export default function EpisodeCard( {severity ? ( {severity.name} diff --git a/MobileApp/src/components/IncidentCard.tsx b/MobileApp/src/components/IncidentCard.tsx index 7e207f1544..2dbddfc941 100644 --- a/MobileApp/src/components/IncidentCard.tsx +++ b/MobileApp/src/components/IncidentCard.tsx @@ -3,7 +3,7 @@ import { View, Text, StyleSheet, TouchableOpacity } from "react-native"; import { useTheme } from "../theme"; import { rgbToHex } from "../utils/color"; import { formatRelativeTime } from "../utils/date"; -import type { IncidentItem } from "../api/types"; +import type { IncidentItem, NamedEntity } from "../api/types"; interface IncidentCardProps { incident: IncidentItem; @@ -16,16 +16,16 @@ export default function IncidentCard({ }: IncidentCardProps): React.JSX.Element { const { theme } = useTheme(); - const stateColor = incident.currentIncidentState?.color + const stateColor: string = incident.currentIncidentState?.color ? rgbToHex(incident.currentIncidentState.color) : theme.colors.textTertiary; - const severityColor = incident.incidentSeverity?.color + const severityColor: string = incident.incidentSeverity?.color ? rgbToHex(incident.incidentSeverity.color) : theme.colors.textTertiary; - const monitorCount = incident.monitors?.length ?? 0; - const timeString = formatRelativeTime( + const monitorCount: number = incident.monitors?.length ?? 0; + const timeString: string = formatRelativeTime( incident.declaredAt || incident.createdAt, ); @@ -44,12 +44,7 @@ export default function IncidentCard({ accessibilityLabel={`Incident ${incident.incidentNumberWithPrefix || incident.incidentNumber}, ${incident.title}. State: ${incident.currentIncidentState?.name ?? "unknown"}. Severity: ${incident.incidentSeverity?.name ?? "unknown"}.`} > - + {incident.incidentNumberWithPrefix || `#${incident.incidentNumber}`} @@ -76,7 +71,9 @@ export default function IncidentCard({ ]} > - + {incident.currentIncidentState.name} @@ -84,10 +81,7 @@ export default function IncidentCard({ {incident.incidentSeverity ? ( {incident.incidentSeverity.name} @@ -101,14 +95,18 @@ export default function IncidentCard({ style={[styles.monitors, { color: theme.colors.textSecondary }]} numberOfLines={1} > - {incident.monitors.map((m) => m.name).join(", ")} + {incident.monitors + .map((m: NamedEntity) => { + return m.name; + }) + .join(", ")} ) : null} ); } -const styles = StyleSheet.create({ +const styles: ReturnType = StyleSheet.create({ card: { padding: 16, borderRadius: 12, diff --git a/MobileApp/src/components/ProjectBadge.tsx b/MobileApp/src/components/ProjectBadge.tsx index bcb31026d9..9799c940a8 100644 --- a/MobileApp/src/components/ProjectBadge.tsx +++ b/MobileApp/src/components/ProjectBadge.tsx @@ -13,7 +13,7 @@ export default function ProjectBadge({ }: ProjectBadgeProps): React.JSX.Element { const { theme } = useTheme(); - const dotColor = color || theme.colors.actionPrimary; + const dotColor: string = color || theme.colors.actionPrimary; return ( @@ -31,7 +31,7 @@ export default function ProjectBadge({ ); } -const styles = StyleSheet.create({ +const styles: ReturnType = StyleSheet.create({ container: { flexDirection: "row", alignItems: "center", diff --git a/MobileApp/src/components/SeverityBadge.tsx b/MobileApp/src/components/SeverityBadge.tsx index 71bb2d10bc..1d487abc29 100644 --- a/MobileApp/src/components/SeverityBadge.tsx +++ b/MobileApp/src/components/SeverityBadge.tsx @@ -2,12 +2,7 @@ import React from "react"; import { View, Text, StyleSheet } from "react-native"; import { useTheme } from "../theme"; -export type SeverityLevel = - | "critical" - | "major" - | "minor" - | "warning" - | "info"; +export type SeverityLevel = "critical" | "major" | "minor" | "warning" | "info"; interface SeverityBadgeProps { severity: SeverityLevel; @@ -43,8 +38,8 @@ export default function SeverityBadge({ }, }; - const colors = colorMap[severity]; - const displayLabel = label || severity; + const colors: { text: string; bg: string } = colorMap[severity]; + const displayLabel: string = label || severity; return ( @@ -55,7 +50,7 @@ export default function SeverityBadge({ ); } -const styles = StyleSheet.create({ +const styles: ReturnType = StyleSheet.create({ badge: { paddingHorizontal: 8, paddingVertical: 4, diff --git a/MobileApp/src/components/SkeletonCard.tsx b/MobileApp/src/components/SkeletonCard.tsx index 4c2e345cea..c95ccdf871 100644 --- a/MobileApp/src/components/SkeletonCard.tsx +++ b/MobileApp/src/components/SkeletonCard.tsx @@ -18,11 +18,11 @@ export default function SkeletonCard({ variant = "card", }: SkeletonCardProps): React.JSX.Element { const { theme } = useTheme(); - const opacity = useRef(new Animated.Value(0.3)).current; - const reduceMotion = useRef(false); + const opacity: Animated.Value = useRef(new Animated.Value(0.3)).current; + const reduceMotion: React.MutableRefObject = useRef(false); useEffect(() => { - AccessibilityInfo.isReduceMotionEnabled().then((enabled) => { + AccessibilityInfo.isReduceMotionEnabled().then((enabled: boolean) => { reduceMotion.current = enabled; }); }, []); @@ -33,7 +33,7 @@ export default function SkeletonCard({ return; } - const animation = Animated.loop( + const animation: Animated.CompositeAnimation = Animated.loop( Animated.sequence([ Animated.timing(opacity, { toValue: 0.7, @@ -98,10 +98,7 @@ export default function SkeletonCard({ if (variant === "detail") { return ( @@ -137,22 +134,24 @@ export default function SkeletonCard({ }, ]} > - {Array.from({ length: 3 }).map((_, index) => ( - - - - - ))} + {Array.from({ length: 3 }).map((_: unknown, index: number) => { + return ( + + + + + ); + })} ); @@ -209,23 +208,25 @@ export default function SkeletonCard({ /> {/* Body lines */} - {Array.from({ length: Math.max(lines - 1, 1) }).map((_, index) => ( - - ))} + {Array.from({ length: Math.max(lines - 1, 1) }).map((_: unknown, index: number) => { + return ( + + ); + })} ); } -const styles = StyleSheet.create({ +const styles: ReturnType = StyleSheet.create({ card: { padding: 16, borderRadius: 12, diff --git a/MobileApp/src/components/StateBadge.tsx b/MobileApp/src/components/StateBadge.tsx index 15816481a6..bbfb609813 100644 --- a/MobileApp/src/components/StateBadge.tsx +++ b/MobileApp/src/components/StateBadge.tsx @@ -28,8 +28,8 @@ export default function StateBadge({ muted: theme.colors.stateMuted, }; - const color = colorMap[state]; - const displayLabel = label || state; + const color: string = colorMap[state]; + const displayLabel: string = label || state; return ( - + {displayLabel.charAt(0).toUpperCase() + displayLabel.slice(1)} ); } -const styles = StyleSheet.create({ +const styles: ReturnType = StyleSheet.create({ badge: { flexDirection: "row", alignItems: "center", diff --git a/MobileApp/src/components/SwipeableCard.tsx b/MobileApp/src/components/SwipeableCard.tsx index 970230912d..8287879126 100644 --- a/MobileApp/src/components/SwipeableCard.tsx +++ b/MobileApp/src/components/SwipeableCard.tsx @@ -5,6 +5,9 @@ import { StyleSheet, Animated, PanResponder, + type GestureResponderEvent, + type PanResponderGestureState, + type PanResponderInstance, } from "react-native"; import { useTheme } from "../theme"; import { useHaptics } from "../hooks/useHaptics"; @@ -21,7 +24,7 @@ interface SwipeableCardProps { rightAction?: SwipeAction; } -const SWIPE_THRESHOLD = 80; +const SWIPE_THRESHOLD: number = 80; export default function SwipeableCard({ children, @@ -30,8 +33,8 @@ export default function SwipeableCard({ }: SwipeableCardProps): React.JSX.Element { const { theme } = useTheme(); const { mediumImpact } = useHaptics(); - const translateX = useRef(new Animated.Value(0)).current; - const hasTriggeredHaptic = useRef(false); + const translateX: Animated.Value = useRef(new Animated.Value(0)).current; + const hasTriggeredHaptic: React.MutableRefObject = useRef(false); const panResponder = useRef( PanResponder.create({ @@ -90,20 +93,14 @@ export default function SwipeableCard({ {leftAction ? ( {leftAction.label} ) : null} {rightAction ? ( {rightAction.label} diff --git a/MobileApp/src/hooks/useAlertDetail.ts b/MobileApp/src/hooks/useAlertDetail.ts index 373353a44d..f15e1bd5f9 100644 --- a/MobileApp/src/hooks/useAlertDetail.ts +++ b/MobileApp/src/hooks/useAlertDetail.ts @@ -8,23 +8,29 @@ import { export function useAlertDetail(projectId: string, alertId: string) { return useQuery({ queryKey: ["alert", projectId, alertId], - queryFn: () => fetchAlertById(projectId, alertId), - enabled: !!projectId && !!alertId, + queryFn: () => { + return fetchAlertById(projectId, alertId); + }, + enabled: Boolean(projectId) && Boolean(alertId), }); } export function useAlertStates(projectId: string) { return useQuery({ queryKey: ["alert-states", projectId], - queryFn: () => fetchAlertStates(projectId), - enabled: !!projectId, + queryFn: () => { + return fetchAlertStates(projectId); + }, + enabled: Boolean(projectId), }); } export function useAlertStateTimeline(projectId: string, alertId: string) { return useQuery({ queryKey: ["alert-state-timeline", projectId, alertId], - queryFn: () => fetchAlertStateTimeline(projectId, alertId), - enabled: !!projectId && !!alertId, + queryFn: () => { + return fetchAlertStateTimeline(projectId, alertId); + }, + enabled: Boolean(projectId) && Boolean(alertId), }); } diff --git a/MobileApp/src/hooks/useAlertEpisodeDetail.ts b/MobileApp/src/hooks/useAlertEpisodeDetail.ts index 2baa6e0ad8..9a86843532 100644 --- a/MobileApp/src/hooks/useAlertEpisodeDetail.ts +++ b/MobileApp/src/hooks/useAlertEpisodeDetail.ts @@ -6,22 +6,23 @@ import { fetchAlertEpisodeNotes, } from "../api/alertEpisodes"; -export function useAlertEpisodeDetail( - projectId: string, - episodeId: string, -) { +export function useAlertEpisodeDetail(projectId: string, episodeId: string) { return useQuery({ queryKey: ["alert-episode", projectId, episodeId], - queryFn: () => fetchAlertEpisodeById(projectId, episodeId), - enabled: !!projectId && !!episodeId, + queryFn: () => { + return fetchAlertEpisodeById(projectId, episodeId); + }, + enabled: Boolean(projectId) && Boolean(episodeId), }); } export function useAlertEpisodeStates(projectId: string) { return useQuery({ queryKey: ["alert-states", projectId], - queryFn: () => fetchAlertEpisodeStates(projectId), - enabled: !!projectId, + queryFn: () => { + return fetchAlertEpisodeStates(projectId); + }, + enabled: Boolean(projectId), }); } @@ -31,18 +32,19 @@ export function useAlertEpisodeStateTimeline( ) { return useQuery({ queryKey: ["alert-episode-state-timeline", projectId, episodeId], - queryFn: () => fetchAlertEpisodeStateTimeline(projectId, episodeId), - enabled: !!projectId && !!episodeId, + queryFn: () => { + return fetchAlertEpisodeStateTimeline(projectId, episodeId); + }, + enabled: Boolean(projectId) && Boolean(episodeId), }); } -export function useAlertEpisodeNotes( - projectId: string, - episodeId: string, -) { +export function useAlertEpisodeNotes(projectId: string, episodeId: string) { return useQuery({ queryKey: ["alert-episode-notes", projectId, episodeId], - queryFn: () => fetchAlertEpisodeNotes(projectId, episodeId), - enabled: !!projectId && !!episodeId, + queryFn: () => { + return fetchAlertEpisodeNotes(projectId, episodeId); + }, + enabled: Boolean(projectId) && Boolean(episodeId), }); } diff --git a/MobileApp/src/hooks/useAlertEpisodes.ts b/MobileApp/src/hooks/useAlertEpisodes.ts index 4f4ac61df6..1c225aded0 100644 --- a/MobileApp/src/hooks/useAlertEpisodes.ts +++ b/MobileApp/src/hooks/useAlertEpisodes.ts @@ -8,8 +8,10 @@ export function useAlertEpisodes( ) { return useQuery({ queryKey: ["alert-episodes", projectId, skip, limit], - queryFn: () => fetchAlertEpisodes(projectId, { skip, limit }), - enabled: !!projectId, + queryFn: () => { + return fetchAlertEpisodes(projectId, { skip, limit }); + }, + enabled: Boolean(projectId), }); } @@ -24,6 +26,6 @@ export function useUnresolvedAlertEpisodeCount(projectId: string) { }); return response.count; }, - enabled: !!projectId, + enabled: Boolean(projectId), }); } diff --git a/MobileApp/src/hooks/useAlertNotes.ts b/MobileApp/src/hooks/useAlertNotes.ts index c1e08e8d0d..12a3c5761c 100644 --- a/MobileApp/src/hooks/useAlertNotes.ts +++ b/MobileApp/src/hooks/useAlertNotes.ts @@ -4,7 +4,9 @@ import { fetchAlertNotes } from "../api/alertNotes"; export function useAlertNotes(projectId: string, alertId: string) { return useQuery({ queryKey: ["alert-notes", projectId, alertId], - queryFn: () => fetchAlertNotes(projectId, alertId), - enabled: !!projectId && !!alertId, + queryFn: () => { + return fetchAlertNotes(projectId, alertId); + }, + enabled: Boolean(projectId) && Boolean(alertId), }); } diff --git a/MobileApp/src/hooks/useAlerts.ts b/MobileApp/src/hooks/useAlerts.ts index 105a480f3e..13bd970cf3 100644 --- a/MobileApp/src/hooks/useAlerts.ts +++ b/MobileApp/src/hooks/useAlerts.ts @@ -8,8 +8,10 @@ export function useAlerts( ) { return useQuery({ queryKey: ["alerts", projectId, skip, limit], - queryFn: () => fetchAlerts(projectId, { skip, limit }), - enabled: !!projectId, + queryFn: () => { + return fetchAlerts(projectId, { skip, limit }); + }, + enabled: Boolean(projectId), }); } @@ -24,6 +26,6 @@ export function useUnresolvedAlertCount(projectId: string) { }); return response.count; }, - enabled: !!projectId, + enabled: Boolean(projectId), }); } diff --git a/MobileApp/src/hooks/useAuth.tsx b/MobileApp/src/hooks/useAuth.tsx index ad4c70c697..a462997715 100644 --- a/MobileApp/src/hooks/useAuth.tsx +++ b/MobileApp/src/hooks/useAuth.tsx @@ -6,7 +6,7 @@ import React, { useCallback, ReactNode, } from "react"; -import { getTokens, clearTokens } from "../storage/keychain"; +import { getTokens } from "../storage/keychain"; import { hasServerUrl } from "../storage/serverUrl"; import { login as apiLogin, @@ -33,7 +33,9 @@ interface AuthProviderProps { children: ReactNode; } -export function AuthProvider({ children }: AuthProviderProps): React.JSX.Element { +export function AuthProvider({ + children, +}: AuthProviderProps): React.JSX.Element { const [isAuthenticated, setIsAuthenticated] = useState(false); const [isLoading, setIsLoading] = useState(true); const [needsServerUrl, setNeedsServerUrl] = useState(false); diff --git a/MobileApp/src/hooks/useIncidentDetail.ts b/MobileApp/src/hooks/useIncidentDetail.ts index eec3671075..b5c9541876 100644 --- a/MobileApp/src/hooks/useIncidentDetail.ts +++ b/MobileApp/src/hooks/useIncidentDetail.ts @@ -8,16 +8,20 @@ import { export function useIncidentDetail(projectId: string, incidentId: string) { return useQuery({ queryKey: ["incident", projectId, incidentId], - queryFn: () => fetchIncidentById(projectId, incidentId), - enabled: !!projectId && !!incidentId, + queryFn: () => { + return fetchIncidentById(projectId, incidentId); + }, + enabled: Boolean(projectId) && Boolean(incidentId), }); } export function useIncidentStates(projectId: string) { return useQuery({ queryKey: ["incident-states", projectId], - queryFn: () => fetchIncidentStates(projectId), - enabled: !!projectId, + queryFn: () => { + return fetchIncidentStates(projectId); + }, + enabled: Boolean(projectId), }); } @@ -27,7 +31,9 @@ export function useIncidentStateTimeline( ) { return useQuery({ queryKey: ["incident-state-timeline", projectId, incidentId], - queryFn: () => fetchIncidentStateTimeline(projectId, incidentId), - enabled: !!projectId && !!incidentId, + queryFn: () => { + return fetchIncidentStateTimeline(projectId, incidentId); + }, + enabled: Boolean(projectId) && Boolean(incidentId), }); } diff --git a/MobileApp/src/hooks/useIncidentEpisodeDetail.ts b/MobileApp/src/hooks/useIncidentEpisodeDetail.ts index 354702ef19..620b90fb16 100644 --- a/MobileApp/src/hooks/useIncidentEpisodeDetail.ts +++ b/MobileApp/src/hooks/useIncidentEpisodeDetail.ts @@ -6,22 +6,23 @@ import { fetchIncidentEpisodeNotes, } from "../api/incidentEpisodes"; -export function useIncidentEpisodeDetail( - projectId: string, - episodeId: string, -) { +export function useIncidentEpisodeDetail(projectId: string, episodeId: string) { return useQuery({ queryKey: ["incident-episode", projectId, episodeId], - queryFn: () => fetchIncidentEpisodeById(projectId, episodeId), - enabled: !!projectId && !!episodeId, + queryFn: () => { + return fetchIncidentEpisodeById(projectId, episodeId); + }, + enabled: Boolean(projectId) && Boolean(episodeId), }); } export function useIncidentEpisodeStates(projectId: string) { return useQuery({ queryKey: ["incident-states", projectId], - queryFn: () => fetchIncidentEpisodeStates(projectId), - enabled: !!projectId, + queryFn: () => { + return fetchIncidentEpisodeStates(projectId); + }, + enabled: Boolean(projectId), }); } @@ -31,18 +32,19 @@ export function useIncidentEpisodeStateTimeline( ) { return useQuery({ queryKey: ["incident-episode-state-timeline", projectId, episodeId], - queryFn: () => fetchIncidentEpisodeStateTimeline(projectId, episodeId), - enabled: !!projectId && !!episodeId, + queryFn: () => { + return fetchIncidentEpisodeStateTimeline(projectId, episodeId); + }, + enabled: Boolean(projectId) && Boolean(episodeId), }); } -export function useIncidentEpisodeNotes( - projectId: string, - episodeId: string, -) { +export function useIncidentEpisodeNotes(projectId: string, episodeId: string) { return useQuery({ queryKey: ["incident-episode-notes", projectId, episodeId], - queryFn: () => fetchIncidentEpisodeNotes(projectId, episodeId), - enabled: !!projectId && !!episodeId, + queryFn: () => { + return fetchIncidentEpisodeNotes(projectId, episodeId); + }, + enabled: Boolean(projectId) && Boolean(episodeId), }); } diff --git a/MobileApp/src/hooks/useIncidentEpisodes.ts b/MobileApp/src/hooks/useIncidentEpisodes.ts index 05cbbe9bf4..0573bb2354 100644 --- a/MobileApp/src/hooks/useIncidentEpisodes.ts +++ b/MobileApp/src/hooks/useIncidentEpisodes.ts @@ -8,8 +8,10 @@ export function useIncidentEpisodes( ) { return useQuery({ queryKey: ["incident-episodes", projectId, skip, limit], - queryFn: () => fetchIncidentEpisodes(projectId, { skip, limit }), - enabled: !!projectId, + queryFn: () => { + return fetchIncidentEpisodes(projectId, { skip, limit }); + }, + enabled: Boolean(projectId), }); } @@ -24,6 +26,6 @@ export function useUnresolvedIncidentEpisodeCount(projectId: string) { }); return response.count; }, - enabled: !!projectId, + enabled: Boolean(projectId), }); } diff --git a/MobileApp/src/hooks/useIncidentNotes.ts b/MobileApp/src/hooks/useIncidentNotes.ts index 81380ea594..03a725a4fd 100644 --- a/MobileApp/src/hooks/useIncidentNotes.ts +++ b/MobileApp/src/hooks/useIncidentNotes.ts @@ -4,7 +4,9 @@ import { fetchIncidentNotes } from "../api/incidentNotes"; export function useIncidentNotes(projectId: string, incidentId: string) { return useQuery({ queryKey: ["incident-notes", projectId, incidentId], - queryFn: () => fetchIncidentNotes(projectId, incidentId), - enabled: !!projectId && !!incidentId, + queryFn: () => { + return fetchIncidentNotes(projectId, incidentId); + }, + enabled: Boolean(projectId) && Boolean(incidentId), }); } diff --git a/MobileApp/src/hooks/useIncidents.ts b/MobileApp/src/hooks/useIncidents.ts index d63e8b135a..4ee507141e 100644 --- a/MobileApp/src/hooks/useIncidents.ts +++ b/MobileApp/src/hooks/useIncidents.ts @@ -8,8 +8,10 @@ export function useIncidents( ) { return useQuery({ queryKey: ["incidents", projectId, skip, limit], - queryFn: () => fetchIncidents(projectId, { skip, limit }), - enabled: !!projectId, + queryFn: () => { + return fetchIncidents(projectId, { skip, limit }); + }, + enabled: Boolean(projectId), }); } @@ -24,6 +26,6 @@ export function useUnresolvedIncidentCount(projectId: string) { }); return response.count; }, - enabled: !!projectId, + enabled: Boolean(projectId), }); } diff --git a/MobileApp/src/hooks/useProject.tsx b/MobileApp/src/hooks/useProject.tsx index 4cdb737668..cf6d3ce6a5 100644 --- a/MobileApp/src/hooks/useProject.tsx +++ b/MobileApp/src/hooks/useProject.tsx @@ -21,7 +21,9 @@ interface ProjectContextValue { clearProject: () => Promise; } -const ProjectContext = createContext(undefined); +const ProjectContext = createContext( + undefined, +); interface ProjectProviderProps { children: ReactNode; @@ -45,9 +47,9 @@ export function ProjectProvider({ // Try to restore previously selected project const savedId = await AsyncStorage.getItem(PROJECT_STORAGE_KEY); if (savedId) { - const saved = response.data.find( - (p: ProjectItem) => p._id === savedId, - ); + const saved = response.data.find((p: ProjectItem) => { + return p._id === savedId; + }); if (saved) { setSelectedProject(saved); } diff --git a/MobileApp/src/hooks/usePushNotifications.ts b/MobileApp/src/hooks/usePushNotifications.ts index c0e6b9a730..d7f030d466 100644 --- a/MobileApp/src/hooks/usePushNotifications.ts +++ b/MobileApp/src/hooks/usePushNotifications.ts @@ -17,9 +17,7 @@ import { useProject } from "./useProject"; const PUSH_TOKEN_KEY = "oneuptime_expo_push_token"; -export function usePushNotifications( - navigationRef: unknown, -): void { +export function usePushNotifications(navigationRef: unknown): void { const { isAuthenticated } = useAuth(); const { projectList } = useProject(); const responseListenerRef = useRef(null); @@ -79,10 +77,11 @@ export function usePushNotifications( // Set up notification listeners useEffect(() => { - receivedListenerRef.current = - Notifications.addNotificationReceivedListener((_notification) => { + receivedListenerRef.current = Notifications.addNotificationReceivedListener( + (_notification) => { // Foreground notification received — handler in setup.ts shows it - }); + }, + ); responseListenerRef.current = Notifications.addNotificationResponseReceivedListener( diff --git a/MobileApp/src/navigation/MainTabNavigator.tsx b/MobileApp/src/navigation/MainTabNavigator.tsx index 9f783d1b91..be77e0c7dd 100644 --- a/MobileApp/src/navigation/MainTabNavigator.tsx +++ b/MobileApp/src/navigation/MainTabNavigator.tsx @@ -9,7 +9,7 @@ import AlertEpisodesStackNavigator from "./AlertEpisodesStackNavigator"; import SettingsStackNavigator from "./SettingsStackNavigator"; import { useTheme } from "../theme"; -const Tab = 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 a6ae161b5f..f3197d2f15 100644 --- a/MobileApp/src/navigation/RootNavigator.tsx +++ b/MobileApp/src/navigation/RootNavigator.tsx @@ -119,7 +119,9 @@ export default function RootNavigator(): React.JSX.Element { if (biometric.isEnabled && !biometricPassed) { return ( setBiometricPassed(true)} + onSuccess={() => { + return setBiometricPassed(true); + }} biometricType={biometric.biometricType} /> ); diff --git a/MobileApp/src/notifications/setup.ts b/MobileApp/src/notifications/setup.ts index 97b4d04e2c..451c76b217 100644 --- a/MobileApp/src/notifications/setup.ts +++ b/MobileApp/src/notifications/setup.ts @@ -5,13 +5,15 @@ import { Platform } from "react-native"; // Show notifications when app is in foreground Notifications.setNotificationHandler({ - handleNotification: async () => ({ - shouldShowAlert: true, - shouldPlaySound: true, - shouldSetBadge: true, - shouldShowBanner: true, - shouldShowList: true, - }), + handleNotification: async () => { + return { + shouldShowAlert: true, + shouldPlaySound: true, + shouldSetBadge: true, + shouldShowBanner: true, + shouldShowList: true, + }; + }, }); export async function setupNotificationChannels(): Promise { @@ -24,8 +26,7 @@ export async function setupNotificationChannels(): Promise { importance: Notifications.AndroidImportance.MAX, sound: "default", vibrationPattern: [0, 500, 250, 500], - lockscreenVisibility: - Notifications.AndroidNotificationVisibility.PUBLIC, + lockscreenVisibility: Notifications.AndroidNotificationVisibility.PUBLIC, }); await Notifications.setNotificationChannelAsync("oncall_high", { @@ -80,8 +81,7 @@ export async function requestPermissionsAndGetToken(): Promise { return null; } - const { status: existingStatus } = - await Notifications.getPermissionsAsync(); + const { status: existingStatus } = await Notifications.getPermissionsAsync(); let finalStatus = existingStatus; if (existingStatus !== "granted") { diff --git a/MobileApp/src/screens/AlertDetailScreen.tsx b/MobileApp/src/screens/AlertDetailScreen.tsx index f4f5d0aa36..ea3ebe1140 100644 --- a/MobileApp/src/screens/AlertDetailScreen.tsx +++ b/MobileApp/src/screens/AlertDetailScreen.tsx @@ -23,21 +23,24 @@ import { createAlertNote } from "../api/alertNotes"; import { rgbToHex } from "../utils/color"; import { formatDateTime } from "../utils/date"; import type { AlertsStackParamList } from "../navigation/types"; -import { useQueryClient } from "@tanstack/react-query"; +import { QueryClient, useQueryClient } from "@tanstack/react-query"; +import type { + AlertState, + StateTimelineItem, + NoteItem, +} from "../api/types"; import AddNoteModal from "../components/AddNoteModal"; import SkeletonCard from "../components/SkeletonCard"; import { useHaptics } from "../hooks/useHaptics"; type Props = NativeStackScreenProps; -export default function AlertDetailScreen({ - route, -}: Props): React.JSX.Element { +export default function AlertDetailScreen({ route }: Props): React.JSX.Element { const { alertId } = route.params; const { theme } = useTheme(); const { selectedProject } = useProject(); - const projectId = selectedProject?._id ?? ""; - const queryClient = useQueryClient(); + const projectId: string = selectedProject?._id ?? ""; + const queryClient: QueryClient = useQueryClient(); const { data: alert, @@ -45,32 +48,34 @@ export default function AlertDetailScreen({ refetch: refetchAlert, } = useAlertDetail(projectId, alertId); const { data: states } = useAlertStates(projectId); - const { - data: timeline, - refetch: refetchTimeline, - } = useAlertStateTimeline(projectId, alertId); - const { - data: notes, - refetch: refetchNotes, - } = useAlertNotes(projectId, alertId); + const { data: timeline, refetch: refetchTimeline } = useAlertStateTimeline( + projectId, + alertId, + ); + const { data: notes, refetch: refetchNotes } = useAlertNotes( + projectId, + alertId, + ); const { successFeedback, errorFeedback } = useHaptics(); const [changingState, setChangingState] = useState(false); const [noteModalVisible, setNoteModalVisible] = useState(false); const [submittingNote, setSubmittingNote] = useState(false); - const onRefresh = useCallback(async () => { + const onRefresh: () => Promise = useCallback(async () => { await Promise.all([refetchAlert(), refetchTimeline(), refetchNotes()]); }, [refetchAlert, refetchTimeline, refetchNotes]); - const handleStateChange = useCallback( + const handleStateChange: (stateId: string, stateName: string) => Promise = useCallback( async (stateId: string, stateName: string) => { if (!alert) { return; } - const queryKey = ["alert", projectId, alertId]; - const previousData = queryClient.getQueryData(queryKey); - const newState = states?.find((s) => s._id === stateId); + const queryKey: string[] = ["alert", projectId, alertId]; + const previousData: unknown = queryClient.getQueryData(queryKey); + const newState: AlertState | undefined = states?.find((s: AlertState) => { + return s._id === stateId; + }); if (newState) { queryClient.setQueryData(queryKey, { ...alert, @@ -95,10 +100,18 @@ export default function AlertDetailScreen({ setChangingState(false); } }, - [projectId, alertId, alert, states, refetchAlert, refetchTimeline, queryClient], + [ + projectId, + alertId, + alert, + states, + refetchAlert, + refetchTimeline, + queryClient, + ], ); - const handleAddNote = useCallback( + const handleAddNote: (noteText: string) => Promise = useCallback( async (noteText: string) => { setSubmittingNote(true); try { @@ -117,9 +130,7 @@ export default function AlertDetailScreen({ if (isLoading) { return ( @@ -146,21 +157,25 @@ export default function AlertDetailScreen({ ); } - const stateColor = alert.currentAlertState?.color + const stateColor: string = alert.currentAlertState?.color ? rgbToHex(alert.currentAlertState.color) : theme.colors.textTertiary; - const severityColor = alert.alertSeverity?.color + const severityColor: string = alert.alertSeverity?.color ? rgbToHex(alert.alertSeverity.color) : theme.colors.textTertiary; // Find acknowledge and resolve states from fetched state definitions - const acknowledgeState = states?.find((s) => s.isAcknowledgedState); - const resolveState = states?.find((s) => s.isResolvedState); + const acknowledgeState: AlertState | undefined = states?.find((s: AlertState) => { + return s.isAcknowledgedState; + }); + const resolveState: AlertState | undefined = states?.find((s: AlertState) => { + return s.isResolvedState; + }); - const currentStateId = alert.currentAlertState?._id; - const isResolved = resolveState?._id === currentStateId; - const isAcknowledged = acknowledgeState?._id === currentStateId; + const currentStateId: string | undefined = alert.currentAlertState?._id; + const isResolved: boolean = resolveState?._id === currentStateId; + const isAcknowledged: boolean = acknowledgeState?._id === currentStateId; return ( - + {alert.currentAlertState.name} @@ -215,10 +232,7 @@ export default function AlertDetailScreen({ {alert.description ? ( Description @@ -266,12 +280,18 @@ export default function AlertDetailScreen({ {alert.monitor ? ( Monitor {alert.monitor.name} @@ -284,10 +304,7 @@ export default function AlertDetailScreen({ {!isResolved ? ( Actions @@ -298,18 +315,21 @@ export default function AlertDetailScreen({ styles.actionButton, { backgroundColor: theme.colors.stateAcknowledged }, ]} - onPress={() => - handleStateChange( + onPress={() => { + return handleStateChange( acknowledgeState._id, acknowledgeState.name, - ) - } + ); + }} disabled={changingState} accessibilityRole="button" accessibilityLabel="Acknowledge alert" > {changingState ? ( - + ) : ( - handleStateChange(resolveState._id, resolveState.name) - } + onPress={() => { + return handleStateChange(resolveState._id, resolveState.name); + }} disabled={changingState} accessibilityRole="button" accessibilityLabel="Resolve alert" > {changingState ? ( - + ) : ( 0 ? ( State Timeline - {timeline.map((entry) => { - const entryColor = entry.alertState?.color + {timeline.map((entry: StateTimelineItem) => { + const entryColor: string = entry.alertState?.color ? rgbToHex(entry.alertState.color) : theme.colors.textTertiary; return ( @@ -423,7 +443,9 @@ export default function AlertDetailScreen({ styles.addNoteButton, { backgroundColor: theme.colors.actionPrimary }, ]} - onPress={() => setNoteModalVisible(true)} + onPress={() => { + return setNoteModalVisible(true); + }} > {notes && notes.length > 0 - ? notes.map((note) => ( - - { + return ( + - {note.note} - - - {note.createdByUser ? ( + + {note.note} + + + {note.createdByUser ? ( + + {note.createdByUser.name} + + ) : null} - {note.createdByUser.name} + {formatDateTime(note.createdAt)} - ) : null} - - {formatDateTime(note.createdAt)} - + - - )) + ); + }) : null} {notes && notes.length === 0 ? ( @@ -494,7 +518,9 @@ export default function AlertDetailScreen({ setNoteModalVisible(false)} + onClose={() => { + return setNoteModalVisible(false); + }} onSubmit={handleAddNote} isSubmitting={submittingNote} /> @@ -502,7 +528,7 @@ export default function AlertDetailScreen({ ); } -const styles = StyleSheet.create({ +const styles: ReturnType = StyleSheet.create({ centered: { flex: 1, alignItems: "center", diff --git a/MobileApp/src/screens/AlertEpisodeDetailScreen.tsx b/MobileApp/src/screens/AlertEpisodeDetailScreen.tsx index 4768a85570..2094f5b49c 100644 --- a/MobileApp/src/screens/AlertEpisodeDetailScreen.tsx +++ b/MobileApp/src/screens/AlertEpisodeDetailScreen.tsx @@ -25,7 +25,12 @@ import { import { rgbToHex } from "../utils/color"; import { formatDateTime } from "../utils/date"; import type { AlertEpisodesStackParamList } from "../navigation/types"; -import { useQueryClient } from "@tanstack/react-query"; +import { QueryClient, useQueryClient } from "@tanstack/react-query"; +import type { + AlertState, + StateTimelineItem, + NoteItem, +} from "../api/types"; import AddNoteModal from "../components/AddNoteModal"; import SkeletonCard from "../components/SkeletonCard"; import { useHaptics } from "../hooks/useHaptics"; @@ -41,8 +46,8 @@ export default function AlertEpisodeDetailScreen({ const { episodeId } = route.params; const { theme } = useTheme(); const { selectedProject } = useProject(); - const projectId = selectedProject?._id ?? ""; - const queryClient = useQueryClient(); + const projectId: string = selectedProject?._id ?? ""; + const queryClient: QueryClient = useQueryClient(); const { data: episode, @@ -50,32 +55,32 @@ export default function AlertEpisodeDetailScreen({ refetch: refetchEpisode, } = useAlertEpisodeDetail(projectId, episodeId); const { data: states } = useAlertEpisodeStates(projectId); - const { - data: timeline, - refetch: refetchTimeline, - } = useAlertEpisodeStateTimeline(projectId, episodeId); - const { - data: notes, - refetch: refetchNotes, - } = useAlertEpisodeNotes(projectId, episodeId); + const { data: timeline, refetch: refetchTimeline } = + useAlertEpisodeStateTimeline(projectId, episodeId); + const { data: notes, refetch: refetchNotes } = useAlertEpisodeNotes( + projectId, + episodeId, + ); const { successFeedback, errorFeedback } = useHaptics(); const [changingState, setChangingState] = useState(false); const [noteModalVisible, setNoteModalVisible] = useState(false); const [submittingNote, setSubmittingNote] = useState(false); - const onRefresh = useCallback(async () => { + const onRefresh: () => Promise = useCallback(async () => { await Promise.all([refetchEpisode(), refetchTimeline(), refetchNotes()]); }, [refetchEpisode, refetchTimeline, refetchNotes]); - const handleStateChange = useCallback( + const handleStateChange: (stateId: string, stateName: string) => Promise = useCallback( async (stateId: string, stateName: string) => { if (!episode) { return; } - const queryKey = ["alert-episode", projectId, episodeId]; - const previousData = queryClient.getQueryData(queryKey); - const newState = states?.find((s) => s._id === stateId); + const queryKey: string[] = ["alert-episode", projectId, episodeId]; + const previousData: unknown = queryClient.getQueryData(queryKey); + const newState: AlertState | undefined = states?.find((s: AlertState) => { + return s._id === stateId; + }); if (newState) { queryClient.setQueryData(queryKey, { ...episode, @@ -113,7 +118,7 @@ export default function AlertEpisodeDetailScreen({ ], ); - const handleAddNote = useCallback( + const handleAddNote: (noteText: string) => Promise = useCallback( async (noteText: string) => { setSubmittingNote(true); try { @@ -132,9 +137,7 @@ export default function AlertEpisodeDetailScreen({ if (isLoading) { return ( @@ -161,20 +164,24 @@ export default function AlertEpisodeDetailScreen({ ); } - const stateColor = episode.currentAlertState?.color + const stateColor: string = episode.currentAlertState?.color ? rgbToHex(episode.currentAlertState.color) : theme.colors.textTertiary; - const severityColor = episode.alertSeverity?.color + const severityColor: string = episode.alertSeverity?.color ? rgbToHex(episode.alertSeverity.color) : theme.colors.textTertiary; - const acknowledgeState = states?.find((s) => s.isAcknowledgedState); - const resolveState = states?.find((s) => s.isResolvedState); + const acknowledgeState: AlertState | undefined = states?.find((s: AlertState) => { + return s.isAcknowledgedState; + }); + const resolveState: AlertState | undefined = states?.find((s: AlertState) => { + return s.isResolvedState; + }); - const currentStateId = episode.currentAlertState?._id; - const isResolved = resolveState?._id === currentStateId; - const isAcknowledged = acknowledgeState?._id === currentStateId; + const currentStateId: string | undefined = episode.currentAlertState?._id; + const isResolved: boolean = resolveState?._id === currentStateId; + const isAcknowledged: boolean = acknowledgeState?._id === currentStateId; return ( Description @@ -268,18 +272,12 @@ export default function AlertEpisodeDetailScreen({ > Created {formatDateTime(episode.createdAt)} @@ -287,18 +285,12 @@ export default function AlertEpisodeDetailScreen({ Alerts {episode.alertCount ?? 0} @@ -310,10 +302,7 @@ export default function AlertEpisodeDetailScreen({ {!isResolved ? ( Actions @@ -324,12 +313,12 @@ export default function AlertEpisodeDetailScreen({ styles.actionButton, { backgroundColor: theme.colors.stateAcknowledged }, ]} - onPress={() => - handleStateChange( + onPress={() => { + return handleStateChange( acknowledgeState._id, acknowledgeState.name, - ) - } + ); + }} disabled={changingState} > {changingState ? ( @@ -356,9 +345,9 @@ export default function AlertEpisodeDetailScreen({ styles.actionButton, { backgroundColor: theme.colors.stateResolved }, ]} - onPress={() => - handleStateChange(resolveState._id, resolveState.name) - } + onPress={() => { + return handleStateChange(resolveState._id, resolveState.name); + }} disabled={changingState} > {changingState ? ( @@ -386,15 +375,12 @@ export default function AlertEpisodeDetailScreen({ {timeline && timeline.length > 0 ? ( State Timeline - {timeline.map((entry) => { - const entryColor = entry.alertState?.color + {timeline.map((entry: StateTimelineItem) => { + const entryColor: string = entry.alertState?.color ? rgbToHex(entry.alertState.color) : theme.colors.textTertiary; return ( @@ -409,10 +395,7 @@ export default function AlertEpisodeDetailScreen({ ]} > setNoteModalVisible(true)} + onPress={() => { + return setNoteModalVisible(true); + }} > {notes && notes.length > 0 - ? notes.map((note) => ( - - { + return ( + - {note.note} - - - {note.createdByUser ? ( + + {note.note} + + + {note.createdByUser ? ( + + {note.createdByUser.name} + + ) : null} - {note.createdByUser.name} + {formatDateTime(note.createdAt)} - ) : null} - - {formatDateTime(note.createdAt)} - + - - )) + ); + }) : null} {notes && notes.length === 0 ? ( @@ -528,7 +515,9 @@ export default function AlertEpisodeDetailScreen({ setNoteModalVisible(false)} + onClose={() => { + return setNoteModalVisible(false); + }} onSubmit={handleAddNote} isSubmitting={submittingNote} /> @@ -536,7 +525,7 @@ export default function AlertEpisodeDetailScreen({ ); } -const styles = StyleSheet.create({ +const styles: ReturnType = StyleSheet.create({ centered: { flex: 1, alignItems: "center", diff --git a/MobileApp/src/screens/AlertEpisodesScreen.tsx b/MobileApp/src/screens/AlertEpisodesScreen.tsx index add5490403..416ffc30db 100644 --- a/MobileApp/src/screens/AlertEpisodesScreen.tsx +++ b/MobileApp/src/screens/AlertEpisodesScreen.tsx @@ -19,7 +19,7 @@ import EmptyState from "../components/EmptyState"; import type { AlertEpisodesStackParamList } from "../navigation/types"; import type { AlertEpisodeItem } from "../api/types"; -const PAGE_SIZE = 20; +const PAGE_SIZE: number = 20; type NavProp = NativeStackNavigationProp< AlertEpisodesStackParamList, @@ -29,12 +29,12 @@ type NavProp = NativeStackNavigationProp< export default function AlertEpisodesScreen(): 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 } = useAlertEpisodes( projectId, @@ -42,23 +42,25 @@ export default function AlertEpisodesScreen(): React.JSX.Element { PAGE_SIZE, ); - const episodes = data?.data ?? []; - const totalCount = data?.count ?? 0; - const hasMore = skip + PAGE_SIZE < totalCount; + const episodes: AlertEpisodeItem[] = data?.data ?? []; + const totalCount: number = data?.count ?? 0; + const hasMore: boolean = skip + PAGE_SIZE < totalCount; - const onRefresh = useCallback(async () => { + const onRefresh: () => Promise = useCallback(async () => { lightImpact(); setPage(0); await refetch(); }, [refetch, lightImpact]); - const loadMore = useCallback(() => { + const loadMore: () => void = useCallback(() => { if (hasMore && !isLoading) { - setPage((prev) => prev + 1); + setPage((prev: number) => { + return prev + 1; + }); } }, [hasMore, isLoading]); - const handlePress = useCallback( + const handlePress: (episode: AlertEpisodeItem) => void = useCallback( (episode: AlertEpisodeItem) => { navigation.navigate("AlertEpisodeDetail", { episodeId: episode._id, @@ -105,7 +107,9 @@ export default function AlertEpisodesScreen(): React.JSX.Element { styles.retryButton, { backgroundColor: theme.colors.actionPrimary }, ]} - onPress={() => refetch()} + onPress={() => { + return refetch(); + }} > item._id} + keyExtractor={(item: AlertEpisodeItem) => { + return item._id; + }} contentContainerStyle={ episodes.length === 0 ? styles.emptyContainer : styles.list } - renderItem={({ item }) => ( - handlePress(item)} - /> - )} + renderItem={({ item }: { item: AlertEpisodeItem }) => { + return ( + { + return handlePress(item); + }} + /> + ); + }} ListEmptyComponent={ = StyleSheet.create({ container: { flex: 1, }, diff --git a/MobileApp/src/screens/AlertsScreen.tsx b/MobileApp/src/screens/AlertsScreen.tsx index 8d37b68d48..fd8c66ad74 100644 --- a/MobileApp/src/screens/AlertsScreen.tsx +++ b/MobileApp/src/screens/AlertsScreen.tsx @@ -20,21 +20,21 @@ import SwipeableCard from "../components/SwipeableCard"; import SkeletonCard from "../components/SkeletonCard"; import EmptyState from "../components/EmptyState"; import type { AlertsStackParamList } from "../navigation/types"; -import type { AlertItem } from "../api/types"; -import { useQueryClient } from "@tanstack/react-query"; +import type { AlertItem, AlertState } from "../api/types"; +import { QueryClient, useQueryClient } from "@tanstack/react-query"; -const PAGE_SIZE = 20; +const PAGE_SIZE: number = 20; type NavProp = NativeStackNavigationProp; export default function AlertsScreen(): 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 [page, setPage] = useState(0); - const skip = page * PAGE_SIZE; + const skip: number = page * PAGE_SIZE; const { data, isLoading, isError, refetch } = useAlerts( projectId, @@ -43,34 +43,38 @@ export default function AlertsScreen(): React.JSX.Element { ); const { data: states } = useAlertStates(projectId); const { successFeedback, errorFeedback, lightImpact } = useHaptics(); - const queryClient = useQueryClient(); + const queryClient: QueryClient = useQueryClient(); - const acknowledgeState = states?.find((s) => s.isAcknowledgedState); + const acknowledgeState: AlertState | undefined = states?.find((s: AlertState) => { + return s.isAcknowledgedState; + }); - const alerts = data?.data ?? []; - const totalCount = data?.count ?? 0; - const hasMore = skip + PAGE_SIZE < totalCount; + const alerts: AlertItem[] = data?.data ?? []; + const totalCount: number = data?.count ?? 0; + const hasMore: boolean = skip + PAGE_SIZE < totalCount; - const onRefresh = useCallback(async () => { + const onRefresh: () => Promise = useCallback(async () => { lightImpact(); setPage(0); await refetch(); }, [refetch, lightImpact]); - const loadMore = useCallback(() => { + const loadMore: () => void = useCallback(() => { if (hasMore && !isLoading) { - setPage((prev) => prev + 1); + setPage((prev: number) => { + return prev + 1; + }); } }, [hasMore, isLoading]); - const handlePress = useCallback( + const handlePress: (alert: AlertItem) => void = useCallback( (alert: AlertItem) => { navigation.navigate("AlertDetail", { alertId: alert._id }); }, [navigation], ); - const handleAcknowledge = useCallback( + const handleAcknowledge: (alert: AlertItem) => Promise = useCallback( async (alert: AlertItem) => { if (!acknowledgeState) { return; @@ -84,7 +88,14 @@ export default function AlertsScreen(): React.JSX.Element { await errorFeedback(); } }, - [projectId, acknowledgeState, successFeedback, errorFeedback, refetch, queryClient], + [ + projectId, + acknowledgeState, + successFeedback, + errorFeedback, + refetch, + queryClient, + ], ); if (isLoading && alerts.length === 0) { @@ -125,7 +136,9 @@ export default function AlertsScreen(): React.JSX.Element { styles.retryButton, { backgroundColor: theme.colors.actionPrimary }, ]} - onPress={() => refetch()} + onPress={() => { + return refetch(); + }} > item._id} + keyExtractor={(item: AlertItem) => { + return item._id; + }} contentContainerStyle={ alerts.length === 0 ? styles.emptyContainer : styles.list } - renderItem={({ item }) => ( - handleAcknowledge(item), - } - : undefined - } - > - handlePress(item)} /> - - )} + renderItem={({ item }: { item: AlertItem }) => { + return ( + { + return handleAcknowledge(item); + }, + } + : undefined + } + > + { + return handlePress(item); + }} + /> + + ); + }} ListEmptyComponent={ = StyleSheet.create({ container: { flex: 1, }, diff --git a/MobileApp/src/screens/BiometricLockScreen.tsx b/MobileApp/src/screens/BiometricLockScreen.tsx index 7dcba27807..40b2b1711c 100644 --- a/MobileApp/src/screens/BiometricLockScreen.tsx +++ b/MobileApp/src/screens/BiometricLockScreen.tsx @@ -45,7 +45,10 @@ export default function BiometricLockScreen({ ]} > OneUptime is Locked diff --git a/MobileApp/src/screens/HomeScreen.tsx b/MobileApp/src/screens/HomeScreen.tsx index 095825d654..5ea5a840db 100644 --- a/MobileApp/src/screens/HomeScreen.tsx +++ b/MobileApp/src/screens/HomeScreen.tsx @@ -58,7 +58,7 @@ function StatCard({ accessibilityRole="button" > - {isLoading ? "--" : (count ?? 0)} + {isLoading ? "--" : count ?? 0} {label} @@ -88,12 +88,7 @@ function QuickLink({ label, onPress }: QuickLinkProps): React.JSX.Element { activeOpacity={0.7} accessibilityRole="button" > - + {label} @@ -183,14 +178,18 @@ export default function HomeScreen(): React.JSX.Element { label="Active Incidents" color={theme.colors.severityCritical} isLoading={loadingIncidents} - onPress={() => navigation.navigate("Incidents")} + onPress={() => { + return navigation.navigate("Incidents"); + }} /> navigation.navigate("Alerts")} + onPress={() => { + return navigation.navigate("Alerts"); + }} /> @@ -200,42 +199,51 @@ export default function HomeScreen(): React.JSX.Element { label="Inc Episodes" color={theme.colors.severityCritical} isLoading={loadingIncidentEpisodes} - onPress={() => navigation.navigate("IncidentEpisodes")} + onPress={() => { + return navigation.navigate("IncidentEpisodes"); + }} /> navigation.navigate("AlertEpisodes")} + onPress={() => { + return navigation.navigate("AlertEpisodes"); + }} /> {/* Quick Links */} Quick Links navigation.navigate("Incidents")} + onPress={() => { + return navigation.navigate("Incidents"); + }} /> navigation.navigate("Alerts")} + onPress={() => { + return navigation.navigate("Alerts"); + }} /> navigation.navigate("IncidentEpisodes")} + onPress={() => { + return navigation.navigate("IncidentEpisodes"); + }} /> navigation.navigate("AlertEpisodes")} + onPress={() => { + return navigation.navigate("AlertEpisodes"); + }} /> diff --git a/MobileApp/src/screens/IncidentDetailScreen.tsx b/MobileApp/src/screens/IncidentDetailScreen.tsx index 5b95df1cf7..b85292538f 100644 --- a/MobileApp/src/screens/IncidentDetailScreen.tsx +++ b/MobileApp/src/screens/IncidentDetailScreen.tsx @@ -10,7 +10,7 @@ import { StyleSheet, } from "react-native"; import type { NativeStackScreenProps } from "@react-navigation/native-stack"; -import { useTheme } from "../theme"; +import { useTheme, type Theme } from "../theme"; import { useProject } from "../hooks/useProject"; import { useIncidentDetail, @@ -23,10 +23,17 @@ import { createIncidentNote } from "../api/incidentNotes"; import { rgbToHex } from "../utils/color"; import { formatDateTime } from "../utils/date"; import type { IncidentsStackParamList } from "../navigation/types"; -import { useQueryClient } from "@tanstack/react-query"; +import { QueryClient, useQueryClient } from "@tanstack/react-query"; import AddNoteModal from "../components/AddNoteModal"; import SkeletonCard from "../components/SkeletonCard"; import { useHaptics } from "../hooks/useHaptics"; +import type { + IncidentItem, + IncidentState, + StateTimelineItem, + NoteItem, + NamedEntity, +} from "../api/types"; type Props = NativeStackScreenProps; @@ -34,10 +41,10 @@ export default function IncidentDetailScreen({ route, }: Props): React.JSX.Element { const { incidentId } = route.params; - const { theme } = useTheme(); + const { theme }: { theme: Theme } = useTheme(); const { selectedProject } = useProject(); - const projectId = selectedProject?._id ?? ""; - const queryClient = useQueryClient(); + const projectId: string = selectedProject?._id ?? ""; + const queryClient: QueryClient = useQueryClient(); const { data: incident, @@ -45,32 +52,40 @@ export default function IncidentDetailScreen({ refetch: refetchIncident, } = useIncidentDetail(projectId, incidentId); const { data: states } = useIncidentStates(projectId); - const { - data: timeline, - refetch: refetchTimeline, - } = useIncidentStateTimeline(projectId, incidentId); - const { - data: notes, - refetch: refetchNotes, - } = useIncidentNotes(projectId, incidentId); + const { data: timeline, refetch: refetchTimeline } = useIncidentStateTimeline( + projectId, + incidentId, + ); + const { data: notes, refetch: refetchNotes } = useIncidentNotes( + projectId, + incidentId, + ); const { successFeedback, errorFeedback } = useHaptics(); const [changingState, setChangingState] = useState(false); const [noteModalVisible, setNoteModalVisible] = useState(false); const [submittingNote, setSubmittingNote] = useState(false); - const onRefresh = useCallback(async () => { + const onRefresh: () => Promise = useCallback(async () => { await Promise.all([refetchIncident(), refetchTimeline(), refetchNotes()]); }, [refetchIncident, refetchTimeline, refetchNotes]); - const handleStateChange = useCallback( + const handleStateChange: ( + stateId: string, + stateName: string, + ) => Promise = useCallback( async (stateId: string, stateName: string) => { if (!incident) { return; } - const queryKey = ["incident", projectId, incidentId]; - const previousData = queryClient.getQueryData(queryKey); - const newState = states?.find((s) => s._id === stateId); + const queryKey: string[] = ["incident", projectId, incidentId]; + const previousData: IncidentItem | undefined = + queryClient.getQueryData(queryKey); + const newState: IncidentState | undefined = states?.find( + (s: IncidentState) => { + return s._id === stateId; + }, + ); if (newState) { queryClient.setQueryData(queryKey, { ...incident, @@ -95,10 +110,18 @@ export default function IncidentDetailScreen({ setChangingState(false); } }, - [projectId, incidentId, incident, states, refetchIncident, refetchTimeline, queryClient], + [ + projectId, + incidentId, + incident, + states, + refetchIncident, + refetchTimeline, + queryClient, + ], ); - const handleAddNote = useCallback( + const handleAddNote: (noteText: string) => Promise = useCallback( async (noteText: string) => { setSubmittingNote(true); try { @@ -117,9 +140,7 @@ export default function IncidentDetailScreen({ if (isLoading) { return ( @@ -146,21 +167,29 @@ export default function IncidentDetailScreen({ ); } - const stateColor = incident.currentIncidentState?.color + const stateColor: string = incident.currentIncidentState?.color ? rgbToHex(incident.currentIncidentState.color) : theme.colors.textTertiary; - const severityColor = incident.incidentSeverity?.color + const severityColor: string = incident.incidentSeverity?.color ? rgbToHex(incident.incidentSeverity.color) : theme.colors.textTertiary; // Find acknowledge and resolve states from fetched state definitions - const acknowledgeState = states?.find((s) => s.isAcknowledgedState); - const resolveState = states?.find((s) => s.isResolvedState); + const acknowledgeState: IncidentState | undefined = states?.find( + (s: IncidentState) => { + return s.isAcknowledgedState; + }, + ); + const resolveState: IncidentState | undefined = states?.find( + (s: IncidentState) => { + return s.isResolvedState; + }, + ); - const currentStateId = incident.currentIncidentState?._id; - const isResolved = resolveState?._id === currentStateId; - const isAcknowledged = acknowledgeState?._id === currentStateId; + const currentStateId: string | undefined = incident.currentIncidentState?._id; + const isResolved: boolean = resolveState?._id === currentStateId; + const isAcknowledged: boolean = acknowledgeState?._id === currentStateId; return ( {/* Header */} - + {incident.incidentNumberWithPrefix || `#${incident.incidentNumber}`} @@ -199,7 +223,9 @@ export default function IncidentDetailScreen({ ]} > - + {incident.currentIncidentState.name} @@ -220,10 +246,7 @@ export default function IncidentDetailScreen({ {incident.description ? ( Description @@ -258,7 +281,10 @@ export default function IncidentDetailScreen({ {incident.declaredAt ? ( Declared @@ -289,7 +315,10 @@ export default function IncidentDetailScreen({ {incident.monitors?.length > 0 ? ( Monitors @@ -299,7 +328,11 @@ export default function IncidentDetailScreen({ { color: theme.colors.textPrimary, flex: 1 }, ]} > - {incident.monitors.map((m) => m.name).join(", ")} + {incident.monitors + .map((m: NamedEntity) => { + return m.name; + }) + .join(", ")} ) : null} @@ -310,10 +343,7 @@ export default function IncidentDetailScreen({ {!isResolved ? ( Actions @@ -324,18 +354,21 @@ export default function IncidentDetailScreen({ styles.actionButton, { backgroundColor: theme.colors.stateAcknowledged }, ]} - onPress={() => - handleStateChange( + onPress={() => { + return handleStateChange( acknowledgeState._id, acknowledgeState.name, - ) - } + ); + }} disabled={changingState} accessibilityRole="button" accessibilityLabel="Acknowledge incident" > {changingState ? ( - + ) : ( - handleStateChange(resolveState._id, resolveState.name) - } + onPress={() => { + return handleStateChange(resolveState._id, resolveState.name); + }} disabled={changingState} accessibilityRole="button" accessibilityLabel="Resolve incident" > {changingState ? ( - + ) : ( 0 ? ( 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 ( @@ -449,7 +482,9 @@ export default function IncidentDetailScreen({ styles.addNoteButton, { backgroundColor: theme.colors.actionPrimary }, ]} - onPress={() => setNoteModalVisible(true)} + onPress={() => { + return setNoteModalVisible(true); + }} > {notes && notes.length > 0 - ? notes.map((note) => ( - - { + return ( + - {note.note} - - - {note.createdByUser ? ( + + {note.note} + + + {note.createdByUser ? ( + + {note.createdByUser.name} + + ) : null} - {note.createdByUser.name} + {formatDateTime(note.createdAt)} - ) : null} - - {formatDateTime(note.createdAt)} - + - - )) + ); + }) : null} {notes && notes.length === 0 ? ( @@ -520,7 +557,9 @@ export default function IncidentDetailScreen({ setNoteModalVisible(false)} + onClose={() => { + return setNoteModalVisible(false); + }} onSubmit={handleAddNote} isSubmitting={submittingNote} /> @@ -528,7 +567,7 @@ export default function IncidentDetailScreen({ ); } -const styles = StyleSheet.create({ +const styles: ReturnType = StyleSheet.create({ centered: { flex: 1, alignItems: "center", diff --git a/MobileApp/src/screens/IncidentEpisodeDetailScreen.tsx b/MobileApp/src/screens/IncidentEpisodeDetailScreen.tsx index 9af1ef2cc2..5fdce430e8 100644 --- a/MobileApp/src/screens/IncidentEpisodeDetailScreen.tsx +++ b/MobileApp/src/screens/IncidentEpisodeDetailScreen.tsx @@ -50,14 +50,12 @@ export default function IncidentEpisodeDetailScreen({ refetch: refetchEpisode, } = useIncidentEpisodeDetail(projectId, episodeId); const { data: states } = useIncidentEpisodeStates(projectId); - const { - data: timeline, - refetch: refetchTimeline, - } = useIncidentEpisodeStateTimeline(projectId, episodeId); - const { - data: notes, - refetch: refetchNotes, - } = useIncidentEpisodeNotes(projectId, episodeId); + const { data: timeline, refetch: refetchTimeline } = + useIncidentEpisodeStateTimeline(projectId, episodeId); + const { data: notes, refetch: refetchNotes } = useIncidentEpisodeNotes( + projectId, + episodeId, + ); const { successFeedback, errorFeedback } = useHaptics(); const [changingState, setChangingState] = useState(false); @@ -75,7 +73,9 @@ export default function IncidentEpisodeDetailScreen({ } const queryKey = ["incident-episode", projectId, episodeId]; const previousData = queryClient.getQueryData(queryKey); - const newState = states?.find((s) => s._id === stateId); + const newState = states?.find((s) => { + return s._id === stateId; + }); if (newState) { queryClient.setQueryData(queryKey, { ...episode, @@ -132,9 +132,7 @@ export default function IncidentEpisodeDetailScreen({ if (isLoading) { return ( @@ -169,8 +167,12 @@ export default function IncidentEpisodeDetailScreen({ ? rgbToHex(episode.incidentSeverity.color) : theme.colors.textTertiary; - const acknowledgeState = states?.find((s) => s.isAcknowledgedState); - const resolveState = states?.find((s) => s.isResolvedState); + const acknowledgeState = states?.find((s) => { + return s.isAcknowledgedState; + }); + const resolveState = states?.find((s) => { + return s.isResolvedState; + }); const currentStateId = episode.currentIncidentState?._id; const isResolved = resolveState?._id === currentStateId; @@ -231,10 +233,7 @@ export default function IncidentEpisodeDetailScreen({ {episode.description ? ( Description @@ -289,18 +288,12 @@ export default function IncidentEpisodeDetailScreen({ Created {formatDateTime(episode.createdAt)} @@ -308,18 +301,12 @@ export default function IncidentEpisodeDetailScreen({ Incidents {episode.incidentCount ?? 0} @@ -331,10 +318,7 @@ export default function IncidentEpisodeDetailScreen({ {!isResolved ? ( Actions @@ -345,12 +329,12 @@ export default function IncidentEpisodeDetailScreen({ styles.actionButton, { backgroundColor: theme.colors.stateAcknowledged }, ]} - onPress={() => - handleStateChange( + onPress={() => { + return handleStateChange( acknowledgeState._id, acknowledgeState.name, - ) - } + ); + }} disabled={changingState} > {changingState ? ( @@ -377,9 +361,9 @@ export default function IncidentEpisodeDetailScreen({ styles.actionButton, { backgroundColor: theme.colors.stateResolved }, ]} - onPress={() => - handleStateChange(resolveState._id, resolveState.name) - } + onPress={() => { + return handleStateChange(resolveState._id, resolveState.name); + }} disabled={changingState} > {changingState ? ( @@ -407,10 +391,7 @@ export default function IncidentEpisodeDetailScreen({ {timeline && timeline.length > 0 ? ( State Timeline @@ -430,10 +411,7 @@ export default function IncidentEpisodeDetailScreen({ ]} > setNoteModalVisible(true)} + onPress={() => { + return setNoteModalVisible(true); + }} > {notes && notes.length > 0 - ? notes.map((note) => ( - - { + return ( + - {note.note} - - - {note.createdByUser ? ( + + {note.note} + + + {note.createdByUser ? ( + + {note.createdByUser.name} + + ) : null} - {note.createdByUser.name} + {formatDateTime(note.createdAt)} - ) : null} - - {formatDateTime(note.createdAt)} - + - - )) + ); + }) : null} {notes && notes.length === 0 ? ( @@ -549,7 +531,9 @@ export default function IncidentEpisodeDetailScreen({ setNoteModalVisible(false)} + onClose={() => { + return setNoteModalVisible(false); + }} onSubmit={handleAddNote} isSubmitting={submittingNote} /> diff --git a/MobileApp/src/screens/IncidentEpisodesScreen.tsx b/MobileApp/src/screens/IncidentEpisodesScreen.tsx index 54adef61c8..a767f5f701 100644 --- a/MobileApp/src/screens/IncidentEpisodesScreen.tsx +++ b/MobileApp/src/screens/IncidentEpisodesScreen.tsx @@ -54,7 +54,9 @@ export default function IncidentEpisodesScreen(): React.JSX.Element { const loadMore = useCallback(() => { if (hasMore && !isLoading) { - setPage((prev) => prev + 1); + setPage((prev) => { + return prev + 1; + }); } }, [hasMore, isLoading]); @@ -105,7 +107,9 @@ export default function IncidentEpisodesScreen(): React.JSX.Element { styles.retryButton, { backgroundColor: theme.colors.actionPrimary }, ]} - onPress={() => refetch()} + onPress={() => { + return refetch(); + }} > item._id} + keyExtractor={(item) => { + return item._id; + }} contentContainerStyle={ episodes.length === 0 ? styles.emptyContainer : styles.list } - renderItem={({ item }) => ( - handlePress(item)} - /> - )} + renderItem={({ item }) => { + return ( + { + return handlePress(item); + }} + /> + ); + }} ListEmptyComponent={ ; +type NavProp = NativeStackNavigationProp< + IncidentsStackParamList, + "IncidentsList" +>; export default function IncidentsScreen(): React.JSX.Element { - const { theme } = useTheme(); + const { theme }: { theme: Theme } = useTheme(); const { selectedProject } = useProject(); - const projectId = selectedProject?._id ?? ""; - const navigation = useNavigation(); + const projectId: string = selectedProject?._id ?? ""; + const navigation: NativeStackNavigationProp< + IncidentsStackParamList, + "IncidentsList" + > = useNavigation(); const [page, setPage] = useState(0); - const skip = page * PAGE_SIZE; + const skip: number = page * PAGE_SIZE; const { data, isLoading, isError, refetch } = useIncidents( projectId, @@ -43,49 +49,67 @@ export default function IncidentsScreen(): React.JSX.Element { ); const { data: states } = useIncidentStates(projectId); const { successFeedback, errorFeedback, lightImpact } = useHaptics(); - const queryClient = useQueryClient(); + const queryClient: QueryClient = useQueryClient(); - const acknowledgeState = states?.find((s) => s.isAcknowledgedState); + const acknowledgeState: IncidentState | undefined = states?.find( + (s: IncidentState) => { + return s.isAcknowledgedState; + }, + ); - const incidents = data?.data ?? []; - const totalCount = data?.count ?? 0; - const hasMore = skip + PAGE_SIZE < totalCount; + const incidents: IncidentItem[] = data?.data ?? []; + const totalCount: number = data?.count ?? 0; + const hasMore: boolean = skip + PAGE_SIZE < totalCount; - const onRefresh = useCallback(async () => { + const onRefresh: () => Promise = useCallback(async () => { lightImpact(); setPage(0); await refetch(); }, [refetch, lightImpact]); - const loadMore = useCallback(() => { + const loadMore: () => void = useCallback(() => { if (hasMore && !isLoading) { - setPage((prev) => prev + 1); + setPage((prev: number) => { + return prev + 1; + }); } }, [hasMore, isLoading]); - const handlePress = useCallback( + const handlePress: (incident: IncidentItem) => void = useCallback( (incident: IncidentItem) => { navigation.navigate("IncidentDetail", { incidentId: incident._id }); }, [navigation], ); - const handleAcknowledge = useCallback( - async (incident: IncidentItem) => { - if (!acknowledgeState) { - return; - } - try { - await changeIncidentState(projectId, incident._id, acknowledgeState._id); - await successFeedback(); - await refetch(); - await queryClient.invalidateQueries({ queryKey: ["incidents"] }); - } catch { - await errorFeedback(); - } - }, - [projectId, acknowledgeState, successFeedback, errorFeedback, refetch, queryClient], - ); + const handleAcknowledge: (incident: IncidentItem) => Promise = + useCallback( + async (incident: IncidentItem) => { + if (!acknowledgeState) { + return; + } + try { + await changeIncidentState( + projectId, + incident._id, + acknowledgeState._id, + ); + await successFeedback(); + await refetch(); + await queryClient.invalidateQueries({ queryKey: ["incidents"] }); + } catch { + await errorFeedback(); + } + }, + [ + projectId, + acknowledgeState, + successFeedback, + errorFeedback, + refetch, + queryClient, + ], + ); if (isLoading && incidents.length === 0) { return ( @@ -125,7 +149,9 @@ export default function IncidentsScreen(): React.JSX.Element { styles.retryButton, { backgroundColor: theme.colors.actionPrimary }, ]} - onPress={() => refetch()} + onPress={() => { + return refetch(); + }} > item._id} + keyExtractor={(item: IncidentItem) => { + return item._id; + }} contentContainerStyle={ incidents.length === 0 ? styles.emptyContainer : styles.list } - renderItem={({ item }) => ( - handleAcknowledge(item), - } - : undefined - } - > - handlePress(item)} /> - - )} + renderItem={({ item }: { item: IncidentItem }) => { + return ( + { + return handleAcknowledge(item); + }, + } + : undefined + } + > + { + return handlePress(item); + }} + /> + + ); + }} ListEmptyComponent={ = StyleSheet.create({ container: { flex: 1, }, diff --git a/MobileApp/src/screens/NotificationPreferencesScreen.tsx b/MobileApp/src/screens/NotificationPreferencesScreen.tsx index 1a1ed3a30c..f3c789232c 100644 --- a/MobileApp/src/screens/NotificationPreferencesScreen.tsx +++ b/MobileApp/src/screens/NotificationPreferencesScreen.tsx @@ -1,11 +1,5 @@ import React, { useState, useEffect, useCallback } from "react"; -import { - View, - Text, - ScrollView, - Switch, - StyleSheet, -} from "react-native"; +import { View, Text, ScrollView, Switch, StyleSheet, ViewStyle, TextStyle } from "react-native"; import { useTheme } from "../theme"; import { useHaptics } from "../hooks/useHaptics"; import { @@ -43,9 +37,7 @@ function PrefRow({ accessibilityLabel={`${label}. ${description}`} > - + {label} { - getNotificationPreferences().then((p) => { + getNotificationPreferences().then((p: NotificationPreferences) => { setPrefs(p); setLoaded(true); }); }, []); - const updatePref = useCallback( + const updatePref: (key: keyof NotificationPreferences, value: boolean) => void = useCallback( (key: keyof NotificationPreferences, value: boolean) => { selectionFeedback(); - const updated = { ...prefs, [key]: value }; + const updated: NotificationPreferences = { ...prefs, [key]: value }; setPrefs(updated); setNotificationPreferences(updated); }, @@ -130,25 +122,33 @@ export default function NotificationPreferencesScreen(): React.JSX.Element { label="Incidents" description="New incidents and state changes" value={prefs.incidents} - onValueChange={(v) => updatePref("incidents", v)} + onValueChange={(v: boolean) => { + return updatePref("incidents", v); + }} /> updatePref("alerts", v)} + onValueChange={(v: boolean) => { + return updatePref("alerts", v); + }} /> updatePref("incidentEpisodes", v)} + onValueChange={(v: boolean) => { + return updatePref("incidentEpisodes", v); + }} /> updatePref("alertEpisodes", v)} + onValueChange={(v: boolean) => { + return updatePref("alertEpisodes", v); + }} /> @@ -166,25 +166,39 @@ export default function NotificationPreferencesScreen(): React.JSX.Element { label="Critical Only" description="Only receive notifications for critical and high severity events" value={prefs.criticalOnly} - onValueChange={(v) => updatePref("criticalOnly", v)} + onValueChange={(v: boolean) => { + return updatePref("criticalOnly", v); + }} /> {/* Info */} - - Notification preferences are stored locally on this device. Server-side - notification rules configured in your project settings take precedence. + + Notification preferences are stored locally on this device. + Server-side notification rules configured in your project settings + take precedence. ); } -const styles = StyleSheet.create({ +const styles: { + container: ViewStyle; + content: ViewStyle; + section: ViewStyle; + sectionTitle: TextStyle; + sectionHint: TextStyle; + rowGroup: ViewStyle; + row: ViewStyle; + rowText: ViewStyle; + rowLabel: TextStyle; + rowDescription: TextStyle; + infoSection: ViewStyle; + infoText: TextStyle; +} = StyleSheet.create({ container: { flex: 1, }, diff --git a/MobileApp/src/screens/ProjectSelectionScreen.tsx b/MobileApp/src/screens/ProjectSelectionScreen.tsx index a4d59fd2b4..e377bb2b52 100644 --- a/MobileApp/src/screens/ProjectSelectionScreen.tsx +++ b/MobileApp/src/screens/ProjectSelectionScreen.tsx @@ -98,7 +98,10 @@ export default function ProjectSelectionScreen(): React.JSX.Element { > Select Project @@ -114,48 +117,54 @@ export default function ProjectSelectionScreen(): React.JSX.Element { item._id} + keyExtractor={(item) => { + return item._id; + }} contentContainerStyle={styles.list} - renderItem={({ item }) => ( - handleSelect(item)} - activeOpacity={0.7} - > - { + return ( + - - { + return handleSelect(item); + }} + activeOpacity={0.7} + > + - {item.name} - - {item.slug ? ( + /> + - {item.slug} + {item.name} - ) : null} - - - )} + {item.slug ? ( + + {item.slug} + + ) : null} + + + ); + }} /> ); diff --git a/MobileApp/src/screens/SettingsScreen.tsx b/MobileApp/src/screens/SettingsScreen.tsx index ac23a98d31..3bfeb2c624 100644 --- a/MobileApp/src/screens/SettingsScreen.tsx +++ b/MobileApp/src/screens/SettingsScreen.tsx @@ -17,7 +17,10 @@ import { useHaptics } from "../hooks/useHaptics"; import { getServerUrl } from "../storage/serverUrl"; import type { SettingsStackParamList } from "../navigation/types"; -type SettingsNavProp = NativeStackNavigationProp; +type SettingsNavProp = NativeStackNavigationProp< + SettingsStackParamList, + "SettingsList" +>; const APP_VERSION = "1.0.0"; @@ -64,7 +67,9 @@ function SettingsRow({ {rightElement ?? (value ? ( - + {value} ) : onPress ? ( @@ -147,16 +152,16 @@ export default function SettingsScreen(): React.JSX.Element { backgroundColor: theme.colors.actionPrimary, }, ]} - onPress={() => handleThemeChange(mode)} + onPress={() => { + return handleThemeChange(mode); + }} activeOpacity={0.7} > @@ -166,9 +171,7 @@ export default function SettingsScreen(): React.JSX.Element { style={[ styles.themeOptionLabel, { - color: isActive - ? "#FFFFFF" - : theme.colors.textPrimary, + color: isActive ? "#FFFFFF" : theme.colors.textPrimary, }, ]} > @@ -189,7 +192,9 @@ export default function SettingsScreen(): React.JSX.Element { navigation.navigate("NotificationPreferences")} + onPress={() => { + return navigation.navigate("NotificationPreferences"); + }} /> @@ -245,10 +250,7 @@ export default function SettingsScreen(): React.JSX.Element { > Server - + {/* Account */} @@ -258,11 +260,7 @@ export default function SettingsScreen(): React.JSX.Element { > Account - + {/* About */} @@ -279,9 +277,7 @@ export default function SettingsScreen(): React.JSX.Element { {/* Footer branding */} - + OneUptime On-Call diff --git a/MobileApp/src/screens/auth/LoginScreen.tsx b/MobileApp/src/screens/auth/LoginScreen.tsx index d438974fbb..461d7e6104 100644 --- a/MobileApp/src/screens/auth/LoginScreen.tsx +++ b/MobileApp/src/screens/auth/LoginScreen.tsx @@ -9,9 +9,12 @@ import { KeyboardAvoidingView, Platform, ScrollView, + ViewStyle, + TextStyle, } from "react-native"; import { useTheme } from "../../theme"; import { useAuth } from "../../hooks/useAuth"; +import { LoginResponse } from "../../api/auth"; import { getServerUrl } from "../../storage/serverUrl"; import { NativeStackNavigationProp } from "@react-navigation/native-stack"; import { useNavigation } from "@react-navigation/native"; @@ -25,7 +28,7 @@ type LoginNavigationProp = NativeStackNavigationProp< export default function LoginScreen(): React.JSX.Element { const { theme } = useTheme(); const { login, setNeedsServerUrl } = useAuth(); - const navigation = useNavigation(); + const navigation: LoginNavigationProp = useNavigation(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [serverUrl, setServerUrlState] = useState(""); @@ -36,7 +39,7 @@ export default function LoginScreen(): React.JSX.Element { getServerUrl().then(setServerUrlState); }, []); - const handleLogin = async (): Promise => { + const handleLogin: () => Promise = async (): Promise => { if (!email.trim() || !password.trim()) { setError("Email and password are required."); return; @@ -46,7 +49,7 @@ export default function LoginScreen(): React.JSX.Element { setIsLoading(true); try { - const response = await login(email.trim(), password); + const response: LoginResponse = await login(email.trim(), password); if (response.twoFactorRequired) { setError( @@ -54,7 +57,7 @@ export default function LoginScreen(): React.JSX.Element { ); } } catch (err: any) { - const message = + const message: string = err?.response?.data?.message || err?.message || "Login failed. Please check your credentials."; @@ -64,7 +67,7 @@ export default function LoginScreen(): React.JSX.Element { } }; - const handleChangeServer = (): void => { + const handleChangeServer: () => void = (): void => { setNeedsServerUrl(true); navigation.navigate("ServerUrl"); }; @@ -123,7 +126,7 @@ export default function LoginScreen(): React.JSX.Element { }, ]} value={email} - onChangeText={(text) => { + onChangeText={(text: string) => { setEmail(text); setError(null); }} @@ -158,7 +161,7 @@ export default function LoginScreen(): React.JSX.Element { }, ]} value={password} - onChangeText={(text) => { + onChangeText={(text: string) => { setPassword(text); setError(null); }} @@ -229,7 +232,16 @@ export default function LoginScreen(): React.JSX.Element { ); } -const styles = StyleSheet.create({ +const styles: { + flex: ViewStyle; + scrollContent: ViewStyle; + container: ViewStyle; + header: ViewStyle; + form: ViewStyle; + input: TextStyle; + button: ViewStyle; + changeServer: ViewStyle; +} = StyleSheet.create({ flex: { flex: 1, }, diff --git a/MobileApp/src/screens/auth/ServerUrlScreen.tsx b/MobileApp/src/screens/auth/ServerUrlScreen.tsx index 19e86a67a6..4becba4544 100644 --- a/MobileApp/src/screens/auth/ServerUrlScreen.tsx +++ b/MobileApp/src/screens/auth/ServerUrlScreen.tsx @@ -9,6 +9,8 @@ import { KeyboardAvoidingView, Platform, ScrollView, + ViewStyle, + TextStyle, } from "react-native"; import { NativeStackNavigationProp } from "@react-navigation/native-stack"; import { useNavigation } from "@react-navigation/native"; @@ -26,12 +28,12 @@ type ServerUrlNavigationProp = NativeStackNavigationProp< export default function ServerUrlScreen(): React.JSX.Element { const { theme } = useTheme(); const { setNeedsServerUrl } = useAuth(); - const navigation = useNavigation(); + const navigation: ServerUrlNavigationProp = useNavigation(); const [url, setUrl] = useState("https://oneuptime.com"); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const handleConnect = async (): Promise => { + const handleConnect: () => Promise = async (): Promise => { if (!url.trim()) { setError("Please enter a server URL"); return; @@ -41,8 +43,8 @@ export default function ServerUrlScreen(): React.JSX.Element { setIsLoading(true); try { - const normalizedUrl = url.trim().replace(/\/+$/, ""); - const isValid = await validateServerUrl(normalizedUrl); + const normalizedUrl: string = url.trim().replace(/\/+$/, ""); + const isValid: boolean = await validateServerUrl(normalizedUrl); if (!isValid) { setError( @@ -118,7 +120,7 @@ export default function ServerUrlScreen(): React.JSX.Element { }, ]} value={url} - onChangeText={(text) => { + onChangeText={(text: string) => { setUrl(text); setError(null); }} @@ -189,7 +191,15 @@ export default function ServerUrlScreen(): React.JSX.Element { ); } -const styles = StyleSheet.create({ +const styles: { + flex: ViewStyle; + scrollContent: ViewStyle; + container: ViewStyle; + header: ViewStyle; + form: ViewStyle; + input: TextStyle; + button: ViewStyle; +} = StyleSheet.create({ flex: { flex: 1, }, diff --git a/MobileApp/src/storage/keychain.ts b/MobileApp/src/storage/keychain.ts index 5974acc893..4b6421883d 100644 --- a/MobileApp/src/storage/keychain.ts +++ b/MobileApp/src/storage/keychain.ts @@ -1,6 +1,6 @@ import * as Keychain from "react-native-keychain"; -const SERVICE_NAME = "com.oneuptime.oncall.tokens"; +const SERVICE_NAME: string = "com.oneuptime.oncall.tokens"; export interface StoredTokens { accessToken: string; @@ -17,15 +17,13 @@ export function getCachedAccessToken(): string | null { export async function storeTokens(tokens: StoredTokens): Promise { cachedAccessToken = tokens.accessToken; - await Keychain.setGenericPassword( - "tokens", - JSON.stringify(tokens), - { service: SERVICE_NAME }, - ); + await Keychain.setGenericPassword("tokens", JSON.stringify(tokens), { + service: SERVICE_NAME, + }); } export async function getTokens(): Promise { - const credentials = await Keychain.getGenericPassword({ + const credentials: false | Keychain.UserCredentials = await Keychain.getGenericPassword({ service: SERVICE_NAME, }); diff --git a/MobileApp/src/storage/preferences.ts b/MobileApp/src/storage/preferences.ts index 308dc6b5e4..678581745e 100644 --- a/MobileApp/src/storage/preferences.ts +++ b/MobileApp/src/storage/preferences.ts @@ -1,7 +1,11 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import type { ThemeMode } from "../theme"; -const KEYS = { +const KEYS: { + readonly THEME_MODE: "oneuptime_theme_mode"; + readonly BIOMETRIC_ENABLED: "oneuptime_biometric_enabled"; + readonly NOTIFICATION_PREFS: "oneuptime_notification_prefs"; +} = { THEME_MODE: "oneuptime_theme_mode", BIOMETRIC_ENABLED: "oneuptime_biometric_enabled", NOTIFICATION_PREFS: "oneuptime_notification_prefs", @@ -24,7 +28,7 @@ const DEFAULT_NOTIFICATION_PREFS: NotificationPreferences = { }; export async function getThemeMode(): Promise { - const stored = await AsyncStorage.getItem(KEYS.THEME_MODE); + const stored: string | null = await AsyncStorage.getItem(KEYS.THEME_MODE); if (stored === "dark" || stored === "light" || stored === "system") { return stored; } @@ -36,7 +40,7 @@ export async function setThemeMode(mode: ThemeMode): Promise { } export async function getBiometricEnabled(): Promise { - const stored = await AsyncStorage.getItem(KEYS.BIOMETRIC_ENABLED); + const stored: string | null = await AsyncStorage.getItem(KEYS.BIOMETRIC_ENABLED); return stored === "true"; } @@ -45,7 +49,7 @@ export async function setBiometricEnabled(enabled: boolean): Promise { } export async function getNotificationPreferences(): Promise { - const stored = 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/storage/serverUrl.ts b/MobileApp/src/storage/serverUrl.ts index 3a60e45d28..63ca2b95cf 100644 --- a/MobileApp/src/storage/serverUrl.ts +++ b/MobileApp/src/storage/serverUrl.ts @@ -1,14 +1,14 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; -const STORAGE_KEY = "oneuptime_server_url"; -const DEFAULT_SERVER_URL = "https://oneuptime.com"; +const STORAGE_KEY: string = "oneuptime_server_url"; +const DEFAULT_SERVER_URL: string = "https://oneuptime.com"; function normalizeUrl(url: string): string { return url.replace(/\/+$/, ""); } export async function getServerUrl(): Promise { - const stored = await AsyncStorage.getItem(STORAGE_KEY); + const stored: string | null = await AsyncStorage.getItem(STORAGE_KEY); return stored || DEFAULT_SERVER_URL; } @@ -17,7 +17,7 @@ export async function setServerUrl(url: string): Promise { } export async function hasServerUrl(): Promise { - const stored = await AsyncStorage.getItem(STORAGE_KEY); + const stored: string | null = await AsyncStorage.getItem(STORAGE_KEY); return stored !== null; } diff --git a/MobileApp/src/theme/ThemeContext.tsx b/MobileApp/src/theme/ThemeContext.tsx index b5f5b8ff4c..2cd8e36a29 100644 --- a/MobileApp/src/theme/ThemeContext.tsx +++ b/MobileApp/src/theme/ThemeContext.tsx @@ -31,29 +31,31 @@ interface ThemeContextValue { setThemeMode: (mode: ThemeMode) => void; } -const ThemeContext = createContext(undefined); +const ThemeContext: React.Context = createContext(undefined); interface ThemeProviderProps { children: ReactNode; } -export function ThemeProvider({ children }: ThemeProviderProps): React.JSX.Element { - const systemColorScheme = useColorScheme(); +export function ThemeProvider({ + children, +}: ThemeProviderProps): React.JSX.Element { + const systemColorScheme: "light" | "dark" | null | undefined = useColorScheme(); const [themeMode, setThemeModeState] = useState("dark"); // Load persisted theme on mount useEffect(() => { - loadThemeMode().then((mode) => { + loadThemeMode().then((mode: ThemeMode) => { setThemeModeState(mode); }); }, []); - const setThemeMode = (mode: ThemeMode): void => { + const setThemeMode: (mode: ThemeMode) => void = (mode: ThemeMode): void => { setThemeModeState(mode); saveThemeMode(mode); }; - const theme = useMemo((): Theme => { + const theme: Theme = useMemo((): Theme => { let isDark: boolean; if (themeMode === "system") { @@ -71,14 +73,13 @@ export function ThemeProvider({ children }: ThemeProviderProps): React.JSX.Eleme }; }, [themeMode, systemColorScheme]); - const value = useMemo( - (): ThemeContextValue => ({ + const value: ThemeContextValue = useMemo((): ThemeContextValue => { + return { theme, themeMode, setThemeMode, - }), - [theme, themeMode], - ); + }; + }, [theme, themeMode]); return ( {children} @@ -86,7 +87,7 @@ export function ThemeProvider({ children }: ThemeProviderProps): React.JSX.Eleme } export function useTheme(): ThemeContextValue { - const context = useContext(ThemeContext); + const context: ThemeContextValue | undefined = useContext(ThemeContext); if (!context) { throw new Error("useTheme must be used within a ThemeProvider"); } diff --git a/MobileApp/src/theme/spacing.ts b/MobileApp/src/theme/spacing.ts index fa9c89c2c6..be4e67b6a6 100644 --- a/MobileApp/src/theme/spacing.ts +++ b/MobileApp/src/theme/spacing.ts @@ -1,4 +1,10 @@ -export const spacing = { +export const spacing: { + readonly xs: 4; + readonly sm: 8; + readonly md: 16; + readonly lg: 24; + readonly xl: 32; +} = { xs: 4, sm: 8, md: 16, @@ -6,7 +12,11 @@ export const spacing = { xl: 32, } as const; -export const radius = { +export const radius: { + readonly sm: 6; + readonly md: 12; + readonly lg: 16; +} = { sm: 6, md: 12, lg: 16, diff --git a/MobileApp/src/utils/color.ts b/MobileApp/src/utils/color.ts index d21e6d934c..a25c72475c 100644 --- a/MobileApp/src/utils/color.ts +++ b/MobileApp/src/utils/color.ts @@ -1,11 +1,7 @@ -export function rgbToHex(color: { - r: number; - g: number; - b: number; -}): string { - const r = Math.max(0, Math.min(255, Math.round(color.r))); - const g = Math.max(0, Math.min(255, Math.round(color.g))); - const b = Math.max(0, Math.min(255, Math.round(color.b))); +export function rgbToHex(color: { r: number; g: number; b: number }): string { + const r: number = Math.max(0, Math.min(255, Math.round(color.r))); + const g: number = Math.max(0, Math.min(255, Math.round(color.g))); + const b: number = Math.max(0, Math.min(255, Math.round(color.b))); return ( "#" + diff --git a/MobileApp/src/utils/date.ts b/MobileApp/src/utils/date.ts index 5395fbbc81..429cdbb3d8 100644 --- a/MobileApp/src/utils/date.ts +++ b/MobileApp/src/utils/date.ts @@ -1,38 +1,38 @@ export function formatRelativeTime(dateString: string): string { - const now = Date.now(); - const date = new Date(dateString).getTime(); - const seconds = Math.floor((now - date) / 1000); + const now: number = Date.now(); + const date: number = new Date(dateString).getTime(); + const seconds: number = Math.floor((now - date) / 1000); if (seconds < 60) { return "just now"; } - const minutes = Math.floor(seconds / 60); + const minutes: number = Math.floor(seconds / 60); if (minutes < 60) { return `${minutes}m ago`; } - const hours = Math.floor(minutes / 60); + const hours: number = Math.floor(minutes / 60); if (hours < 24) { return `${hours}h ago`; } - const days = Math.floor(hours / 24); + const days: number = Math.floor(hours / 24); if (days < 30) { return `${days}d ago`; } - const months = Math.floor(days / 30); + const months: number = Math.floor(days / 30); if (months < 12) { return `${months}mo ago`; } - const years = Math.floor(months / 12); + const years: number = Math.floor(months / 12); return `${years}y ago`; } export function formatDateTime(dateString: string): string { - const date = new Date(dateString); + const date: Date = new Date(dateString); return date.toLocaleDateString(undefined, { year: "numeric", month: "short",