/** * Notification Settings Screen * Per-device push notification configuration */ import { View, ScrollView, Switch, Pressable, ActivityIndicator, Alert } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Bell, ShieldAlert, Play, Square, Monitor, Smartphone, AlertTriangle, ServerCrash, ServerCog, Moon, Flame, type LucideIcon, } from 'lucide-react-native'; import { Text } from '@/components/ui/text'; import { Card } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import { api } from '@/lib/api'; import { useAuthStore } from '@/lib/authStore'; import { colors } from '@/lib/theme'; import type { NotificationPreferences } from '@tracearr/shared'; // Rule types for violation filtering const RULE_TYPES = [ { value: 'impossible_travel', label: 'Impossible Travel' }, { value: 'simultaneous_locations', label: 'Simultaneous Locations' }, { value: 'device_velocity', label: 'Device Velocity' }, { value: 'concurrent_streams', label: 'Concurrent Streams' }, { value: 'geo_restriction', label: 'Geo Restriction' }, ] as const; // Severity levels const SEVERITY_LEVELS = [ { value: 1, label: 'All (Low, Warning, High)' }, { value: 2, label: 'Warning & High only' }, { value: 3, label: 'High severity only' }, ] as const; function Divider() { return ; } function SettingsSection({ title, children, }: { title: string; children: React.ReactNode; }) { return ( {title} {children} ); } function SettingRow({ icon: Icon, label, description, value, onValueChange, disabled, }: { icon?: LucideIcon; label: string; description?: string; value: boolean; onValueChange: (value: boolean) => void; disabled?: boolean; }) { return ( {Icon && ( )} {label} {description && ( {description} )} ); } function SelectRow({ label, value, options, onChange, disabled, }: { label: string; value: number; options: ReadonlyArray<{ value: number; label: string }>; onChange: (value: number) => void; disabled?: boolean; }) { const currentOption = options.find((o) => o.value === value); const handlePress = () => { if (disabled) return; Alert.alert( label, undefined, options.map((option) => ({ text: option.label, onPress: () => onChange(option.value), style: option.value === value ? 'cancel' : 'default', })) ); }; return ( {label} {currentOption?.label ?? 'Select...'} ); } function MultiSelectRow({ selectedValues, options, onChange, disabled, }: { selectedValues: string[]; options: ReadonlyArray<{ value: string; label: string }>; onChange: (values: string[]) => void; disabled?: boolean; }) { const toggleValue = (value: string) => { if (disabled) return; if (selectedValues.includes(value)) { onChange(selectedValues.filter((v) => v !== value)); } else { onChange([...selectedValues, value]); } }; const allSelected = selectedValues.length === 0; return ( onChange([])} disabled={disabled} className={cn( 'px-3 py-1.5 rounded-full border', allSelected ? 'bg-cyan-core border-cyan-core' : 'border-border bg-card', disabled && 'opacity-50' )} > All Types {options.map((option) => { const isSelected = selectedValues.includes(option.value); return ( toggleValue(option.value)} disabled={disabled} className={cn( 'px-3 py-1.5 rounded-full border', isSelected ? 'bg-cyan-core border-cyan-core' : 'border-border bg-card', disabled && 'opacity-50' )} > {option.label} ); })} ); } function RateLimitStatus({ remainingMinute, remainingHour, maxPerMinute, maxPerHour, }: { remainingMinute?: number; remainingHour?: number; maxPerMinute: number; maxPerHour: number; }) { return ( Current Rate Limit Status Per Minute {remainingMinute ?? maxPerMinute} / {maxPerMinute} Per Hour {remainingHour ?? maxPerHour} / {maxPerHour} ); } export default function NotificationSettingsScreen() { const queryClient = useQueryClient(); const { activeServerId } = useAuthStore(); // Fetch current preferences (per-device, not per-server) const { data: preferences, isLoading, error, } = useQuery({ queryKey: ['notifications', 'preferences'], queryFn: api.notifications.getPreferences, enabled: !!activeServerId, // Still need auth }); // Update mutation with optimistic updates const updateMutation = useMutation({ mutationFn: api.notifications.updatePreferences, onMutate: async (newData) => { await queryClient.cancelQueries({ queryKey: ['notifications', 'preferences'] }); const previousData = queryClient.getQueryData([ 'notifications', 'preferences', ]); queryClient.setQueryData(['notifications', 'preferences'], (old: NotificationPreferences | undefined) => old ? { ...old, ...newData } : old ); return { previousData }; }, onError: (_err, _newData, context) => { if (context?.previousData) { queryClient.setQueryData(['notifications', 'preferences'], context.previousData); } }, onSettled: () => { void queryClient.invalidateQueries({ queryKey: ['notifications', 'preferences'] }); }, }); // Test notification mutation const testMutation = useMutation({ mutationFn: api.notifications.sendTest, onSuccess: (result) => { Alert.alert( result.success ? 'Test Sent' : 'Test Failed', result.message ); }, onError: (error: Error) => { Alert.alert('Error', error.message || 'Failed to send test notification'); }, }); const handleUpdate = ( key: keyof Omit, value: boolean | number | string[] ) => { updateMutation.mutate({ [key]: value }); }; if (isLoading) { return ( Loading preferences... ); } if (error || !preferences) { return ( Unable to Load Preferences {error instanceof Error ? error.message : 'An error occurred'} ); } const pushEnabled = preferences.pushEnabled; return ( {/* Master Toggle */} handleUpdate('pushEnabled', v)} /> {/* Event Toggles */} handleUpdate('onViolationDetected', v)} disabled={!pushEnabled} /> handleUpdate('onStreamStarted', v)} disabled={!pushEnabled} /> handleUpdate('onStreamStopped', v)} disabled={!pushEnabled} /> handleUpdate('onConcurrentStreams', v)} disabled={!pushEnabled} /> handleUpdate('onNewDevice', v)} disabled={!pushEnabled} /> handleUpdate('onTrustScoreChanged', v)} disabled={!pushEnabled} /> handleUpdate('onServerDown', v)} disabled={!pushEnabled} /> handleUpdate('onServerUp', v)} disabled={!pushEnabled} /> {/* Violation Filters - Only show if violation notifications are enabled */} {pushEnabled && preferences.onViolationDetected && ( handleUpdate('violationRuleTypes', values)} /> handleUpdate('violationMinSeverity', value)} /> )} {/* Quiet Hours */} handleUpdate('quietHoursEnabled', v)} disabled={!pushEnabled} /> {pushEnabled && preferences.quietHoursEnabled && ( <> Start Time {preferences.quietHoursStart ?? '23:00'} to End Time {preferences.quietHoursEnd ?? '08:00'} Timezone: {preferences.quietHoursTimezone || 'UTC'} handleUpdate('quietHoursOverrideCritical', v)} /> )} {/* Rate Limiting */} Rate limits prevent notification spam. Current limits: {preferences.maxPerMinute}/min, {preferences.maxPerHour}/hour. {/* Test Notification */} Verify that push notifications are working correctly ); }