feat(push): 新增iOS APNs推送通知功能
- 添加推送通知管理器和设备令牌管理 - 实现推送通知权限请求和令牌注册 - 新增推送通知设置页面 - 集成推送通知初始化到应用启动流程 - 添加推送通知API服务和本地存储管理 - 更新个人页面添加推送通知设置入口
This commit is contained in:
328
app/push-notification-settings.tsx
Normal file
328
app/push-notification-settings.tsx
Normal file
@@ -0,0 +1,328 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user