- 新增iOS原生模块AppStoreReviewManager,封装StoreKit评分请求 - 实现appStoreReviewService服务层,管理评分请求时间间隔(14天) - 在关键用户操作后触发评分请求:完成挑战、记录服药、记录体重、记录饮水 - 优化通知设置页面UI,改进设置项布局和视觉层次 - 调整用药卡片样式,优化状态显示和文字大小 - 新增配置检查脚本check-app-review-setup.sh - 修改喝水提醒默认状态为关闭 评分请求策略: - 仅iOS 14.0+支持 - 自动控制请求频率,避免过度打扰用户 - 延迟1秒执行,不阻塞主业务流程 - 所有评分请求均做错误处理,确保不影响核心功能
435 lines
13 KiB
TypeScript
435 lines
13 KiB
TypeScript
import { HeaderBar } from '@/components/ui/HeaderBar';
|
|
import { palette } from '@/constants/Colors';
|
|
import { useI18n } from '@/hooks/useI18n';
|
|
import { useNotifications } from '@/hooks/useNotifications';
|
|
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
|
|
import {
|
|
getMedicationReminderEnabled,
|
|
getMoodReminderEnabled,
|
|
getNotificationEnabled,
|
|
getNutritionReminderEnabled,
|
|
setMedicationReminderEnabled,
|
|
setMoodReminderEnabled,
|
|
setNotificationEnabled,
|
|
setNutritionReminderEnabled
|
|
} from '@/utils/userPreferences';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
import { router, useFocusEffect } from 'expo-router';
|
|
import React, { useCallback, useState } from 'react';
|
|
import { Alert, Linking, ScrollView, StatusBar, StyleSheet, Switch, Text, View } from 'react-native';
|
|
|
|
export default function NotificationSettingsScreen() {
|
|
const safeAreaTop = useSafeAreaTop(60);
|
|
const { t } = useI18n();
|
|
const { requestPermission, sendNotification } = useNotifications();
|
|
|
|
// 通知设置状态
|
|
const [notificationEnabled, setNotificationEnabledState] = useState(false);
|
|
const [medicationReminderEnabled, setMedicationReminderEnabledState] = useState(false);
|
|
const [nutritionReminderEnabled, setNutritionReminderEnabledState] = useState(false);
|
|
const [moodReminderEnabled, setMoodReminderEnabledState] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
// 加载通知设置
|
|
const loadNotificationSettings = useCallback(async () => {
|
|
try {
|
|
const [notification, medicationReminder, nutritionReminder, moodReminder] = await Promise.all([
|
|
getNotificationEnabled(),
|
|
getMedicationReminderEnabled(),
|
|
getNutritionReminderEnabled(),
|
|
getMoodReminderEnabled(),
|
|
]);
|
|
setNotificationEnabledState(notification);
|
|
setMedicationReminderEnabledState(medicationReminder);
|
|
setNutritionReminderEnabledState(nutritionReminder);
|
|
setMoodReminderEnabledState(moodReminder);
|
|
} catch (error) {
|
|
console.error('Failed to load notification settings:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
// 页面聚焦时加载设置
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
loadNotificationSettings();
|
|
}, [loadNotificationSettings])
|
|
);
|
|
|
|
// 处理总通知开关变化
|
|
const handleNotificationToggle = async (value: boolean) => {
|
|
if (value) {
|
|
try {
|
|
// 先检查系统权限
|
|
const status = await requestPermission();
|
|
if (status === 'granted') {
|
|
// 系统权限获取成功,保存用户偏好设置
|
|
await setNotificationEnabled(true);
|
|
setNotificationEnabledState(true);
|
|
|
|
// 发送测试通知
|
|
await sendNotification({
|
|
title: t('notificationSettings.alerts.notificationsEnabled.title'),
|
|
body: t('notificationSettings.alerts.notificationsEnabled.body'),
|
|
sound: true,
|
|
priority: 'normal',
|
|
});
|
|
} else {
|
|
// 系统权限被拒绝,不更新用户偏好设置
|
|
Alert.alert(
|
|
t('notificationSettings.alerts.permissionDenied.title'),
|
|
t('notificationSettings.alerts.permissionDenied.message'),
|
|
[
|
|
{ text: t('notificationSettings.alerts.permissionDenied.cancel'), style: 'cancel' },
|
|
{ text: t('notificationSettings.alerts.permissionDenied.goToSettings'), onPress: () => Linking.openSettings() }
|
|
]
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to enable push notifications:', error);
|
|
Alert.alert(t('notificationSettings.alerts.error.title'), t('notificationSettings.alerts.error.message'));
|
|
}
|
|
} else {
|
|
try {
|
|
// 关闭推送,保存用户偏好设置
|
|
await setNotificationEnabled(false);
|
|
setNotificationEnabledState(false);
|
|
// 关闭总开关时,也关闭所有提醒
|
|
await setMedicationReminderEnabled(false);
|
|
setMedicationReminderEnabledState(false);
|
|
await setNutritionReminderEnabled(false);
|
|
setNutritionReminderEnabledState(false);
|
|
await setMoodReminderEnabled(false);
|
|
setMoodReminderEnabledState(false);
|
|
} catch (error) {
|
|
console.error('Failed to disable push notifications:', error);
|
|
Alert.alert(t('notificationSettings.alerts.error.title'), t('notificationSettings.alerts.error.saveFailed'));
|
|
}
|
|
}
|
|
};
|
|
|
|
// 处理药品通知提醒开关变化
|
|
const handleMedicationReminderToggle = async (value: boolean) => {
|
|
try {
|
|
await setMedicationReminderEnabled(value);
|
|
setMedicationReminderEnabledState(value);
|
|
|
|
if (value) {
|
|
// 发送测试通知
|
|
await sendNotification({
|
|
title: t('notificationSettings.alerts.medicationReminderEnabled.title'),
|
|
body: t('notificationSettings.alerts.medicationReminderEnabled.body'),
|
|
sound: true,
|
|
priority: 'high',
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to set medication reminder:', error);
|
|
Alert.alert(t('notificationSettings.alerts.error.title'), t('notificationSettings.alerts.error.medicationReminderFailed'));
|
|
}
|
|
};
|
|
|
|
// 处理营养通知提醒开关变化
|
|
const handleNutritionReminderToggle = async (value: boolean) => {
|
|
try {
|
|
await setNutritionReminderEnabled(value);
|
|
setNutritionReminderEnabledState(value);
|
|
|
|
if (value) {
|
|
// 发送测试通知
|
|
await sendNotification({
|
|
title: t('notificationSettings.alerts.nutritionReminderEnabled.title'),
|
|
body: t('notificationSettings.alerts.nutritionReminderEnabled.body'),
|
|
sound: true,
|
|
priority: 'high',
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to set nutrition reminder:', error);
|
|
Alert.alert(t('notificationSettings.alerts.error.title'), t('notificationSettings.alerts.error.nutritionReminderFailed'));
|
|
}
|
|
};
|
|
|
|
// 处理心情通知提醒开关变化
|
|
const handleMoodReminderToggle = async (value: boolean) => {
|
|
try {
|
|
await setMoodReminderEnabled(value);
|
|
setMoodReminderEnabledState(value);
|
|
|
|
if (value) {
|
|
// 发送测试通知
|
|
await sendNotification({
|
|
title: t('notificationSettings.alerts.moodReminderEnabled.title'),
|
|
body: t('notificationSettings.alerts.moodReminderEnabled.body'),
|
|
sound: true,
|
|
priority: 'high',
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to set mood reminder:', error);
|
|
Alert.alert(t('notificationSettings.alerts.error.title'), t('notificationSettings.alerts.error.moodReminderFailed'));
|
|
}
|
|
};
|
|
|
|
// 渲染设置项
|
|
const renderSettingItem = (
|
|
icon: keyof typeof Ionicons.glyphMap,
|
|
title: string,
|
|
description: string,
|
|
value: boolean,
|
|
onValueChange: (value: boolean) => void,
|
|
disabled: boolean = false,
|
|
showSeparator: boolean = true
|
|
) => (
|
|
<View>
|
|
<View style={styles.settingItem}>
|
|
<View style={styles.itemInfo}>
|
|
<View style={[styles.iconContainer, disabled && styles.iconContainerDisabled]}>
|
|
<Ionicons name={icon} size={24} color={disabled ? '#C7C7CC' : '#9370DB'} />
|
|
</View>
|
|
<View style={styles.textContainer}>
|
|
<Text style={[styles.itemTitle, disabled && styles.itemTitleDisabled]}>{title}</Text>
|
|
<Text style={styles.itemDescription} numberOfLines={2}>{description}</Text>
|
|
</View>
|
|
</View>
|
|
<Switch
|
|
value={value}
|
|
onValueChange={onValueChange}
|
|
disabled={disabled}
|
|
trackColor={{ false: '#E5E5E5', true: '#9370DB' }}
|
|
thumbColor="#FFFFFF"
|
|
style={styles.switch}
|
|
/>
|
|
</View>
|
|
{showSeparator && (
|
|
<View style={styles.separatorContainer}>
|
|
<View style={styles.separator} />
|
|
</View>
|
|
)}
|
|
</View>
|
|
);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<View style={styles.container}>
|
|
<StatusBar barStyle="dark-content" backgroundColor="transparent" translucent />
|
|
<LinearGradient
|
|
colors={[palette.purple[100], '#F5F5F5']}
|
|
start={{ x: 1, y: 0 }}
|
|
end={{ x: 0.3, y: 0.4 }}
|
|
style={styles.gradientBackground}
|
|
/>
|
|
<View style={styles.loadingContainer}>
|
|
<Text style={styles.loadingText}>{t('notificationSettings.loading')}</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<StatusBar barStyle="dark-content" backgroundColor="transparent" translucent />
|
|
|
|
<LinearGradient
|
|
colors={[palette.purple[100], '#F5F5F5']}
|
|
start={{ x: 1, y: 0 }}
|
|
end={{ x: 0.3, y: 0.4 }}
|
|
style={styles.gradientBackground}
|
|
/>
|
|
|
|
<HeaderBar
|
|
title={t('notificationSettings.title')}
|
|
onBack={() => router.back()}
|
|
/>
|
|
|
|
<ScrollView
|
|
style={styles.scrollView}
|
|
contentContainerStyle={[
|
|
styles.scrollContent,
|
|
{ paddingTop: safeAreaTop }
|
|
]}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
{/* 顶部说明卡片 */}
|
|
<View style={styles.headerSection}>
|
|
<Text style={styles.subtitle}>{t('notificationSettings.sections.description')}</Text>
|
|
<View style={styles.descriptionCard}>
|
|
<View style={styles.hintRow}>
|
|
<Ionicons name="information-circle-outline" size={20} color="#9370DB" />
|
|
<Text style={styles.descriptionText}>
|
|
{t('notificationSettings.description.text')}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* 设置项列表 */}
|
|
<View style={styles.sectionContainer}>
|
|
{renderSettingItem(
|
|
'notifications-outline',
|
|
t('notificationSettings.items.pushNotifications.title'),
|
|
t('notificationSettings.items.pushNotifications.description'),
|
|
notificationEnabled,
|
|
handleNotificationToggle,
|
|
false,
|
|
true
|
|
)}
|
|
|
|
{renderSettingItem(
|
|
'medkit-outline',
|
|
t('notificationSettings.items.medicationReminder.title'),
|
|
t('notificationSettings.items.medicationReminder.description'),
|
|
medicationReminderEnabled,
|
|
handleMedicationReminderToggle,
|
|
!notificationEnabled,
|
|
true
|
|
)}
|
|
|
|
{renderSettingItem(
|
|
'restaurant-outline',
|
|
t('notificationSettings.items.nutritionReminder.title'),
|
|
t('notificationSettings.items.nutritionReminder.description'),
|
|
nutritionReminderEnabled,
|
|
handleNutritionReminderToggle,
|
|
!notificationEnabled,
|
|
true
|
|
)}
|
|
|
|
{renderSettingItem(
|
|
'happy-outline',
|
|
t('notificationSettings.items.moodReminder.title'),
|
|
t('notificationSettings.items.moodReminder.description'),
|
|
moodReminderEnabled,
|
|
handleMoodReminderToggle,
|
|
!notificationEnabled,
|
|
false
|
|
)}
|
|
</View>
|
|
|
|
</ScrollView>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: '#F5F5F5',
|
|
},
|
|
gradientBackground: {
|
|
position: 'absolute',
|
|
left: 0,
|
|
right: 0,
|
|
top: 0,
|
|
height: '60%',
|
|
},
|
|
scrollView: {
|
|
flex: 1,
|
|
},
|
|
scrollContent: {
|
|
paddingHorizontal: 16,
|
|
paddingBottom: 40,
|
|
},
|
|
loadingContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
loadingText: {
|
|
fontSize: 16,
|
|
color: '#666',
|
|
},
|
|
headerSection: {
|
|
marginBottom: 20,
|
|
},
|
|
subtitle: {
|
|
fontSize: 14,
|
|
color: '#6C757D',
|
|
marginBottom: 12,
|
|
marginLeft: 4,
|
|
},
|
|
descriptionCard: {
|
|
backgroundColor: 'rgba(255, 255, 255, 0.6)',
|
|
borderRadius: 12,
|
|
padding: 12,
|
|
gap: 8,
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(147, 112, 219, 0.1)',
|
|
},
|
|
hintRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
},
|
|
descriptionText: {
|
|
flex: 1,
|
|
fontSize: 13,
|
|
color: '#2C3E50',
|
|
lineHeight: 18,
|
|
},
|
|
sectionContainer: {
|
|
backgroundColor: '#FFFFFF',
|
|
borderRadius: 20,
|
|
marginBottom: 20,
|
|
overflow: 'hidden',
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.03,
|
|
shadowRadius: 8,
|
|
elevation: 2,
|
|
},
|
|
settingItem: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
padding: 16,
|
|
paddingVertical: 16,
|
|
},
|
|
itemInfo: {
|
|
flex: 1,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 12,
|
|
},
|
|
iconContainer: {
|
|
width: 40,
|
|
height: 40,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: 'rgba(147, 112, 219, 0.05)',
|
|
borderRadius: 12,
|
|
},
|
|
iconContainerDisabled: {
|
|
backgroundColor: '#F5F5F5',
|
|
},
|
|
textContainer: {
|
|
flex: 1,
|
|
marginRight: 8,
|
|
},
|
|
itemTitle: {
|
|
fontSize: 16,
|
|
fontWeight: '500',
|
|
color: '#2C3E50',
|
|
marginBottom: 4,
|
|
},
|
|
itemTitleDisabled: {
|
|
color: '#999',
|
|
},
|
|
itemDescription: {
|
|
fontSize: 12,
|
|
color: '#6C757D',
|
|
lineHeight: 16,
|
|
},
|
|
switch: {
|
|
transform: [{ scaleX: 0.9 }, { scaleY: 0.9 }],
|
|
},
|
|
separatorContainer: {
|
|
paddingLeft: 68, // 40(icon) + 12(gap) + 16(padding)
|
|
paddingRight: 16,
|
|
},
|
|
separator: {
|
|
height: 1,
|
|
backgroundColor: '#F0F0F0',
|
|
},
|
|
}); |