WellNuo/app/(tabs)/profile/notifications.tsx
Sergei fe4ff1a932 Simplify DB schema (name/address single fields) + subscription flow
Database:
- Simplified beneficiary schema: single `name` field instead of first_name/last_name
- Single `address` field instead of 5 separate address columns
- Added migration 008_update_notification_settings.sql

Backend:
- Updated all beneficiaries routes for new schema
- Fixed admin routes for simplified fields
- Updated notification settings routes
- Improved stripe and webhook handlers

Frontend:
- Updated all forms to use single name/address fields
- Added new equipment-status and purchase screens
- Added BeneficiaryDetailController service
- Added subscription service
- Improved navigation and auth flow
- Various UI improvements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-08 10:35:15 -08:00

489 lines
15 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
Switch,
TouchableOpacity,
Alert,
ActivityIndicator,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
import { PageHeader } from '@/components/PageHeader';
import { api } from '@/services/api';
import type { NotificationSettings } from '@/types';
interface NotificationSettingProps {
icon: keyof typeof Ionicons.glyphMap;
iconColor: string;
iconBgColor: string;
title: string;
description: string;
value: boolean;
onValueChange: (value: boolean) => void;
}
function NotificationSetting({
icon,
iconColor,
iconBgColor,
title,
description,
value,
onValueChange,
}: NotificationSettingProps) {
return (
<View style={styles.settingRow}>
<View style={[styles.iconContainer, { backgroundColor: iconBgColor }]}>
<Ionicons name={icon} size={20} color={iconColor} />
</View>
<View style={styles.settingContent}>
<Text style={styles.settingTitle}>{title}</Text>
<Text style={styles.settingDescription}>{description}</Text>
</View>
<Switch
value={value}
onValueChange={onValueChange}
trackColor={{ false: '#E5E7EB', true: AppColors.primaryLight }}
thumbColor={value ? AppColors.primary : '#9CA3AF'}
/>
</View>
);
}
// Default settings
const DEFAULT_SETTINGS: NotificationSettings = {
emergencyAlerts: true,
activityAlerts: true,
lowBattery: true,
dailySummary: false,
weeklySummary: true,
pushEnabled: true,
emailEnabled: false,
smsEnabled: false,
quietHours: false,
quietStart: '22:00',
quietEnd: '07:00',
};
export default function NotificationsScreen() {
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
// Alert types
const [emergencyAlerts, setEmergencyAlerts] = useState(DEFAULT_SETTINGS.emergencyAlerts);
const [activityAlerts, setActivityAlerts] = useState(DEFAULT_SETTINGS.activityAlerts);
const [lowBattery, setLowBattery] = useState(DEFAULT_SETTINGS.lowBattery);
const [dailySummary, setDailySummary] = useState(DEFAULT_SETTINGS.dailySummary);
const [weeklySummary, setWeeklySummary] = useState(DEFAULT_SETTINGS.weeklySummary);
// Delivery methods
const [pushEnabled, setPushEnabled] = useState(DEFAULT_SETTINGS.pushEnabled);
const [emailEnabled, setEmailEnabled] = useState(DEFAULT_SETTINGS.emailEnabled);
const [smsEnabled, setSmsEnabled] = useState(DEFAULT_SETTINGS.smsEnabled);
// Quiet hours
const [quietHours, setQuietHours] = useState(DEFAULT_SETTINGS.quietHours);
const [quietStart, setQuietStart] = useState(DEFAULT_SETTINGS.quietStart);
const [quietEnd, setQuietEnd] = useState(DEFAULT_SETTINGS.quietEnd);
// Load settings on mount
useEffect(() => {
loadSettings();
}, []);
const loadSettings = async () => {
try {
const response = await api.getNotificationSettings();
if (response.ok && response.data) {
const s = response.data;
setEmergencyAlerts(s.emergencyAlerts ?? DEFAULT_SETTINGS.emergencyAlerts);
setActivityAlerts(s.activityAlerts ?? DEFAULT_SETTINGS.activityAlerts);
setLowBattery(s.lowBattery ?? DEFAULT_SETTINGS.lowBattery);
setDailySummary(s.dailySummary ?? DEFAULT_SETTINGS.dailySummary);
setWeeklySummary(s.weeklySummary ?? DEFAULT_SETTINGS.weeklySummary);
setPushEnabled(s.pushEnabled ?? DEFAULT_SETTINGS.pushEnabled);
setEmailEnabled(s.emailEnabled ?? DEFAULT_SETTINGS.emailEnabled);
setSmsEnabled(s.smsEnabled ?? DEFAULT_SETTINGS.smsEnabled);
setQuietHours(s.quietHours ?? DEFAULT_SETTINGS.quietHours);
setQuietStart(s.quietStart ?? DEFAULT_SETTINGS.quietStart);
setQuietEnd(s.quietEnd ?? DEFAULT_SETTINGS.quietEnd);
}
} catch (error) {
console.error('Failed to load notification settings:', error);
} finally {
setIsLoading(false);
}
};
const handleSave = async () => {
setIsSaving(true);
try {
const settings: NotificationSettings = {
emergencyAlerts,
activityAlerts,
lowBattery,
dailySummary,
weeklySummary,
pushEnabled,
emailEnabled,
smsEnabled,
quietHours,
quietStart,
quietEnd,
};
const response = await api.updateNotificationSettings(settings);
if (response.ok) {
Alert.alert(
'Settings Saved',
'Your notification preferences have been updated.',
[{ text: 'OK', onPress: () => router.back() }]
);
} else {
Alert.alert('Error', response.error?.message || 'Failed to save settings');
}
} catch (error) {
Alert.alert('Error', 'Failed to save settings. Please try again.');
} finally {
setIsSaving(false);
}
};
const handleQuietHoursConfig = () => {
Alert.alert(
'Quiet Hours',
`Current: ${quietStart} - ${quietEnd}\n\nDuring quiet hours, only emergency alerts will be delivered.`,
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Set Start Time', onPress: () => Alert.alert('Coming Soon', 'Time picker coming soon!') },
]
);
};
if (isLoading) {
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<PageHeader title="Notifications" />
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={AppColors.primary} />
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<PageHeader title="Notifications" />
<ScrollView showsVerticalScrollIndicator={false}>
{/* Alert Types */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Alert Types</Text>
<View style={styles.card}>
<NotificationSetting
icon="warning"
iconColor="#EF4444"
iconBgColor="#FEE2E2"
title="Emergency Alerts"
description="Falls, inactivity, SOS button"
value={emergencyAlerts}
onValueChange={setEmergencyAlerts}
/>
<View style={styles.divider} />
<NotificationSetting
icon="walk"
iconColor="#3B82F6"
iconBgColor="#DBEAFE"
title="Activity Alerts"
description="Unusual activity patterns"
value={activityAlerts}
onValueChange={setActivityAlerts}
/>
<View style={styles.divider} />
<NotificationSetting
icon="battery-half"
iconColor="#F59E0B"
iconBgColor="#FEF3C7"
title="Low Battery"
description="Device battery warnings"
value={lowBattery}
onValueChange={setLowBattery}
/>
<View style={styles.divider} />
<NotificationSetting
icon="today"
iconColor="#10B981"
iconBgColor="#D1FAE5"
title="Daily Summary"
description="Daily wellness report"
value={dailySummary}
onValueChange={setDailySummary}
/>
<View style={styles.divider} />
<NotificationSetting
icon="calendar"
iconColor="#8B5CF6"
iconBgColor="#EDE9FE"
title="Weekly Summary"
description="Weekly health digest"
value={weeklySummary}
onValueChange={setWeeklySummary}
/>
</View>
</View>
{/* Delivery Methods */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Delivery Methods</Text>
<View style={styles.card}>
<NotificationSetting
icon="notifications"
iconColor={AppColors.primary}
iconBgColor="#DBEAFE"
title="Push Notifications"
description="Alerts on your device"
value={pushEnabled}
onValueChange={setPushEnabled}
/>
<View style={styles.divider} />
<NotificationSetting
icon="mail"
iconColor="#6366F1"
iconBgColor="#E0E7FF"
title="Email Notifications"
description="Summaries to your inbox"
value={emailEnabled}
onValueChange={setEmailEnabled}
/>
<View style={styles.divider} />
<NotificationSetting
icon="chatbubble"
iconColor="#EC4899"
iconBgColor="#FCE7F3"
title="SMS Notifications"
description="Text message alerts"
value={smsEnabled}
onValueChange={(value) => {
if (value) {
Alert.alert(
'SMS Notifications',
'SMS notifications require a verified phone number. Would you like to add one?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Add Phone',
onPress: () => router.push('/profile/edit')
},
]
);
} else {
setSmsEnabled(false);
}
}}
/>
</View>
</View>
{/* Quiet Hours */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Quiet Hours</Text>
<View style={styles.card}>
<NotificationSetting
icon="moon"
iconColor="#6366F1"
iconBgColor="#E0E7FF"
title="Enable Quiet Hours"
description="Silence non-emergency alerts"
value={quietHours}
onValueChange={setQuietHours}
/>
{quietHours && (
<>
<View style={styles.divider} />
<TouchableOpacity style={styles.timeRow} onPress={handleQuietHoursConfig}>
<View style={styles.timeInfo}>
<Ionicons name="time-outline" size={20} color={AppColors.textSecondary} />
<Text style={styles.timeLabel}>Quiet Period</Text>
</View>
<View style={styles.timeValue}>
<Text style={styles.timeText}>{quietStart} - {quietEnd}</Text>
<Ionicons name="chevron-forward" size={20} color={AppColors.textMuted} />
</View>
</TouchableOpacity>
</>
)}
</View>
<Text style={styles.quietNote}>
Emergency alerts will always be delivered, even during quiet hours.
</Text>
</View>
{/* Test Notification */}
<View style={styles.section}>
<TouchableOpacity
style={styles.testButton}
onPress={() => {
Alert.alert(
'Test Notification Sent',
'A test push notification has been sent to your device.',
[{ text: 'OK' }]
);
}}
>
<Ionicons name="paper-plane-outline" size={20} color={AppColors.primary} />
<Text style={styles.testButtonText}>Send Test Notification</Text>
</TouchableOpacity>
</View>
</ScrollView>
{/* Save Button */}
<View style={styles.footer}>
<TouchableOpacity
style={[styles.saveButton, isSaving && styles.saveButtonDisabled]}
onPress={handleSave}
disabled={isSaving}
>
{isSaving ? (
<ActivityIndicator color={AppColors.white} />
) : (
<Text style={styles.saveButtonText}>Save Preferences</Text>
)}
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.surface,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
section: {
marginTop: Spacing.md,
},
sectionTitle: {
fontSize: FontSizes.sm,
fontWeight: '600',
color: AppColors.textSecondary,
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.sm,
textTransform: 'uppercase',
},
card: {
backgroundColor: AppColors.background,
},
settingRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: Spacing.md,
paddingHorizontal: Spacing.lg,
},
iconContainer: {
width: 40,
height: 40,
borderRadius: BorderRadius.md,
justifyContent: 'center',
alignItems: 'center',
},
settingContent: {
flex: 1,
marginLeft: Spacing.md,
},
settingTitle: {
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.textPrimary,
},
settingDescription: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: 2,
},
divider: {
height: 1,
backgroundColor: AppColors.border,
marginLeft: Spacing.lg + 40 + Spacing.md,
},
timeRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: Spacing.md,
paddingHorizontal: Spacing.lg,
},
timeInfo: {
flexDirection: 'row',
alignItems: 'center',
},
timeLabel: {
fontSize: FontSizes.base,
color: AppColors.textPrimary,
marginLeft: Spacing.sm,
},
timeValue: {
flexDirection: 'row',
alignItems: 'center',
},
timeText: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
marginRight: Spacing.xs,
},
quietNote: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
paddingHorizontal: Spacing.lg,
paddingTop: Spacing.sm,
},
testButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: AppColors.background,
paddingVertical: Spacing.md,
marginHorizontal: Spacing.lg,
borderRadius: BorderRadius.lg,
borderWidth: 1,
borderColor: AppColors.primary,
},
testButtonText: {
fontSize: FontSizes.base,
fontWeight: '500',
color: AppColors.primary,
marginLeft: Spacing.sm,
},
footer: {
padding: Spacing.lg,
backgroundColor: AppColors.background,
borderTopWidth: 1,
borderTopColor: AppColors.border,
},
saveButton: {
backgroundColor: AppColors.primary,
borderRadius: BorderRadius.lg,
paddingVertical: Spacing.md,
alignItems: 'center',
minHeight: 48,
justifyContent: 'center',
},
saveButtonDisabled: {
opacity: 0.7,
},
saveButtonText: {
fontSize: FontSizes.base,
fontWeight: '600',
color: AppColors.white,
},
});