/**
* 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
);
}