Files
digital-pilates/app/push-notification-settings.tsx
richarjiang 6cdd2fdf9c feat(push): 新增iOS APNs推送通知功能
- 添加推送通知管理器和设备令牌管理
- 实现推送通知权限请求和令牌注册
- 新增推送通知设置页面
- 集成推送通知初始化到应用启动流程
- 添加推送通知API服务和本地存储管理
- 更新个人页面添加推送通知设置入口
2025-10-14 19:25:35 +08:00

328 lines
9.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { getTokenStatusDescription, isPushNotificationAvailable, usePushNotifications } from '@/hooks/usePushNotifications';
import { useThemeColor } from '@/hooks/useThemeColor';
import { TokenStatus } from '@/services/pushNotificationManager';
import React, { useEffect, useState } from 'react';
import {
ActivityIndicator,
Alert,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
export default function PushNotificationSettingsScreen() {
const backgroundColor = useThemeColor({}, 'background');
const textColor = useThemeColor({}, 'text');
const borderColor = useThemeColor({}, 'border');
const primaryColor = useThemeColor({}, 'primary');
const {
isInitialized,
tokenStatus,
isLoading,
registerToken,
getCurrentToken,
updateTokenStatus,
clearAllData,
} = usePushNotifications();
const [isRegistering, setIsRegistering] = useState(false);
const [currentToken, setCurrentToken] = useState<string | null>(null);
useEffect(() => {
updateTokenStatus();
setCurrentToken(getCurrentToken());
}, [isInitialized, tokenStatus]);
const handleRegisterToken = async () => {
setIsRegistering(true);
try {
const success = await registerToken();
if (success) {
Alert.alert('成功', '设备令牌注册成功');
} else {
Alert.alert('失败', '设备令牌注册失败,请检查网络连接和权限设置');
}
} catch (error) {
console.error('注册设备令牌失败:', error);
Alert.alert('错误', '注册设备令牌时发生错误');
} finally {
setIsRegistering(false);
}
};
const handleClearData = () => {
Alert.alert(
'确认清除',
'这将清除所有本地推送通知数据,包括设备令牌。确定要继续吗?',
[
{ text: '取消', style: 'cancel' },
{
text: '确定',
style: 'destructive',
onPress: async () => {
try {
await clearAllData();
Alert.alert('成功', '推送通知数据已清除');
} catch (error) {
console.error('清除数据失败:', error);
Alert.alert('错误', '清除数据时发生错误');
}
},
},
]
);
};
const copyTokenToClipboard = () => {
if (currentToken) {
// 这里可以使用 Clipboard API 复制到剪贴板
Alert.alert('令牌已复制', '设备令牌已复制到剪贴板(功能待实现)');
}
};
const getStatusColor = () => {
switch (tokenStatus) {
case TokenStatus.REGISTERED:
return '#4CAF50';
case TokenStatus.GRANTED:
return '#2196F3';
case TokenStatus.DENIED:
return '#F44336';
case TokenStatus.FAILED:
return '#FF9800';
default:
return '#9E9E9E';
}
};
if (isLoading) {
return (
<ThemedView style={[styles.container, { backgroundColor }]}>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={primaryColor} />
<ThemedText style={styles.loadingText}>...</ThemedText>
</View>
</ThemedView>
);
}
return (
<ThemedView style={[styles.container, { backgroundColor }]}>
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
<View style={styles.header}>
<ThemedText style={styles.title}></ThemedText>
<ThemedText style={styles.subtitle}>
</ThemedText>
</View>
<View style={[styles.card, { borderColor }]}>
<ThemedText style={styles.cardTitle}></ThemedText>
<View style={styles.statusRow}>
<ThemedText style={styles.statusLabel}>:</ThemedText>
<View style={styles.statusValue}>
<Text style={[styles.statusText, { color: isInitialized ? '#4CAF50' : '#F44336' }]}>
{isInitialized ? '已初始化' : '未初始化'}
</Text>
</View>
</View>
<View style={styles.statusRow}>
<ThemedText style={styles.statusLabel}>:</ThemedText>
<View style={styles.statusValue}>
<Text style={[styles.statusText, { color: getStatusColor() }]}>
{getTokenStatusDescription(tokenStatus)}
</Text>
</View>
</View>
<View style={styles.statusRow}>
<ThemedText style={styles.statusLabel}>:</ThemedText>
<View style={styles.statusValue}>
<Text style={[styles.statusText, { color: isPushNotificationAvailable(tokenStatus) ? '#4CAF50' : '#F44336' }]}>
{isPushNotificationAvailable(tokenStatus) ? '是' : '否'}
</Text>
</View>
</View>
</View>
{currentToken && (
<View style={[styles.card, { borderColor }]}>
<ThemedText style={styles.cardTitle}></ThemedText>
<View style={styles.tokenContainer}>
<Text style={[styles.tokenText, { color: textColor }]}>
{currentToken.substring(0, 20)}...{currentToken.substring(currentToken.length - 10)}
</Text>
<TouchableOpacity
style={[styles.copyButton, { borderColor }]}
onPress={copyTokenToClipboard}
>
<Text style={[styles.copyButtonText, { color: primaryColor }]}></Text>
</TouchableOpacity>
</View>
</View>
)}
<View style={[styles.card, { borderColor }]}>
<ThemedText style={styles.cardTitle}></ThemedText>
<TouchableOpacity
style={[styles.actionButton, { backgroundColor: primaryColor }]}
onPress={handleRegisterToken}
disabled={isRegistering}
>
{isRegistering ? (
<ActivityIndicator size="small" color="#ffffff" />
) : (
<Text style={styles.actionButtonText}></Text>
)}
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, styles.secondaryButton, { borderColor }]}
onPress={() => updateTokenStatus()}
>
<Text style={[styles.secondaryButtonText, { color: primaryColor }]}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, styles.dangerButton]}
onPress={handleClearData}
>
<Text style={styles.dangerButtonText}></Text>
</TouchableOpacity>
</View>
<View style={[styles.card, { borderColor }]}>
<ThemedText style={styles.cardTitle}></ThemedText>
<ThemedText style={styles.description}>
iOS设备上使用{'\n'}
{'\n'}
"权限被拒绝"{'\n'}
</ThemedText>
</View>
</ScrollView>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
scrollView: {
flex: 1,
padding: 16,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
marginTop: 16,
fontSize: 16,
},
header: {
marginBottom: 24,
},
title: {
fontSize: 28,
fontWeight: 'bold',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
opacity: 0.7,
},
card: {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
borderRadius: 12,
padding: 16,
marginBottom: 16,
borderWidth: 1,
},
cardTitle: {
fontSize: 18,
fontWeight: '600',
marginBottom: 12,
},
statusRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
statusLabel: {
fontSize: 16,
flex: 1,
},
statusValue: {
flex: 1,
alignItems: 'flex-end',
},
statusText: {
fontSize: 16,
fontWeight: '500',
},
tokenContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
tokenText: {
fontSize: 14,
fontFamily: 'monospace',
flex: 1,
marginRight: 12,
},
copyButton: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 6,
borderWidth: 1,
},
copyButtonText: {
fontSize: 14,
fontWeight: '500',
},
actionButton: {
borderRadius: 8,
paddingVertical: 12,
paddingHorizontal: 16,
alignItems: 'center',
marginBottom: 12,
},
actionButtonText: {
color: '#ffffff',
fontSize: 16,
fontWeight: '600',
},
secondaryButton: {
backgroundColor: 'transparent',
borderWidth: 1,
},
secondaryButtonText: {
fontSize: 16,
fontWeight: '600',
},
dangerButton: {
backgroundColor: '#F44336',
},
dangerButtonText: {
color: '#ffffff',
fontSize: 16,
fontWeight: '600',
},
description: {
fontSize: 14,
lineHeight: 20,
},
});