- Enhanced ChatHeader and ChatInput components to support dynamic theme colors, improving visual consistency across different themes. - Updated styles in ChatScreen to utilize new dynamic styles for better responsiveness and user experience. - Refactored styles in various profile screens to adopt a unified background color scheme and improved spacing for a cleaner layout. This update significantly enhances the chat interface and profile settings by allowing for dynamic theming and improved visual elements.
470 lines
16 KiB
TypeScript
470 lines
16 KiB
TypeScript
import React, { useEffect, useMemo, useState } from 'react';
|
||
import {
|
||
View,
|
||
StyleSheet,
|
||
TextInput,
|
||
TouchableOpacity,
|
||
ActivityIndicator,
|
||
ScrollView,
|
||
} from 'react-native';
|
||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||
import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme';
|
||
import { Text } from '../../components/common';
|
||
import { useResponsive, useResponsiveSpacing } from '../../hooks';
|
||
|
||
// 内容最大宽度
|
||
const CONTENT_MAX_WIDTH = 720;
|
||
import { authService, resolveAuthApiError } from '../../services/authService';
|
||
import { showPrompt } from '../../services/promptService';
|
||
import { useAuthStore } from '../../stores';
|
||
|
||
export const AccountSecurityScreen: React.FC = () => {
|
||
const colors = useAppColors();
|
||
const styles = useMemo(() => createAccountSecurityStyles(colors), [colors]);
|
||
const { isWideScreen, isMobile } = useResponsive();
|
||
const insets = useSafeAreaInsets();
|
||
const currentUser = useAuthStore((state) => state.currentUser);
|
||
const fetchCurrentUser = useAuthStore((state) => state.fetchCurrentUser);
|
||
|
||
// 底部间距,避免被 TabBar 遮挡
|
||
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.md;
|
||
|
||
const [email, setEmail] = useState(currentUser?.email || '');
|
||
const [verificationCode, setVerificationCode] = useState('');
|
||
const [sendingCode, setSendingCode] = useState(false);
|
||
const [verifyingEmail, setVerifyingEmail] = useState(false);
|
||
const [countdown, setCountdown] = useState(0);
|
||
|
||
const [oldPassword, setOldPassword] = useState('');
|
||
const [newPassword, setNewPassword] = useState('');
|
||
const [confirmPassword, setConfirmPassword] = useState('');
|
||
const [changePasswordCode, setChangePasswordCode] = useState('');
|
||
const [sendingChangePwdCode, setSendingChangePwdCode] = useState(false);
|
||
const [changePwdCountdown, setChangePwdCountdown] = useState(0);
|
||
const [updatingPassword, setUpdatingPassword] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (currentUser?.email) {
|
||
setEmail(currentUser.email);
|
||
}
|
||
}, [currentUser?.email]);
|
||
|
||
useEffect(() => {
|
||
if (countdown <= 0) {
|
||
return;
|
||
}
|
||
const timer = setInterval(() => {
|
||
setCountdown((prev) => (prev <= 1 ? 0 : prev - 1));
|
||
}, 1000);
|
||
return () => clearInterval(timer);
|
||
}, [countdown]);
|
||
|
||
useEffect(() => {
|
||
if (changePwdCountdown <= 0) {
|
||
return;
|
||
}
|
||
const timer = setInterval(() => {
|
||
setChangePwdCountdown((prev) => (prev <= 1 ? 0 : prev - 1));
|
||
}, 1000);
|
||
return () => clearInterval(timer);
|
||
}, [changePwdCountdown]);
|
||
|
||
const isEmailVerified = !!currentUser?.email_verified;
|
||
const emailStatusText = useMemo(() => {
|
||
if (!currentUser?.email) {
|
||
return '未绑定邮箱';
|
||
}
|
||
return isEmailVerified ? '已验证' : '未验证';
|
||
}, [currentUser?.email, isEmailVerified]);
|
||
|
||
const validateEmail = (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
||
|
||
const handleSendCode = async () => {
|
||
const targetEmail = email.trim();
|
||
if (!validateEmail(targetEmail)) {
|
||
showPrompt({ title: '提示', message: '请输入正确的邮箱地址', type: 'warning' });
|
||
return;
|
||
}
|
||
if (countdown > 0) {
|
||
return;
|
||
}
|
||
setSendingCode(true);
|
||
try {
|
||
const ok = await authService.sendCurrentUserEmailVerifyCode(targetEmail);
|
||
if (ok) {
|
||
setCountdown(60);
|
||
showPrompt({ title: '发送成功', message: '验证码已发送,请查收邮箱', type: 'success' });
|
||
}
|
||
} catch (error: any) {
|
||
showPrompt({ title: '发送失败', message: resolveAuthApiError(error, '验证码发送失败'), type: 'error' });
|
||
} finally {
|
||
setSendingCode(false);
|
||
}
|
||
};
|
||
|
||
const handleVerifyEmail = async () => {
|
||
const targetEmail = email.trim();
|
||
if (!validateEmail(targetEmail)) {
|
||
showPrompt({ title: '提示', message: '请输入正确的邮箱地址', type: 'warning' });
|
||
return;
|
||
}
|
||
if (!verificationCode.trim()) {
|
||
showPrompt({ title: '提示', message: '请输入验证码', type: 'warning' });
|
||
return;
|
||
}
|
||
|
||
setVerifyingEmail(true);
|
||
try {
|
||
const ok = await authService.verifyCurrentUserEmail({
|
||
email: targetEmail,
|
||
verification_code: verificationCode.trim(),
|
||
});
|
||
if (ok) {
|
||
setVerificationCode('');
|
||
await fetchCurrentUser();
|
||
showPrompt({ title: '验证成功', message: '邮箱已完成验证', type: 'success' });
|
||
}
|
||
} catch (error: any) {
|
||
showPrompt({ title: '验证失败', message: resolveAuthApiError(error, '邮箱验证失败'), type: 'error' });
|
||
} finally {
|
||
setVerifyingEmail(false);
|
||
}
|
||
};
|
||
|
||
const handleChangePassword = async () => {
|
||
if (!oldPassword) {
|
||
showPrompt({ title: '提示', message: '请输入当前密码', type: 'warning' });
|
||
return;
|
||
}
|
||
if (!changePasswordCode.trim()) {
|
||
showPrompt({ title: '提示', message: '请输入邮箱验证码', type: 'warning' });
|
||
return;
|
||
}
|
||
if (newPassword.length < 6) {
|
||
showPrompt({ title: '提示', message: '新密码至少 6 位', type: 'warning' });
|
||
return;
|
||
}
|
||
if (newPassword !== confirmPassword) {
|
||
showPrompt({ title: '提示', message: '两次输入的新密码不一致', type: 'warning' });
|
||
return;
|
||
}
|
||
setUpdatingPassword(true);
|
||
try {
|
||
const ok = await authService.changePassword(oldPassword, newPassword, changePasswordCode.trim());
|
||
if (ok) {
|
||
setOldPassword('');
|
||
setNewPassword('');
|
||
setConfirmPassword('');
|
||
setChangePasswordCode('');
|
||
showPrompt({ title: '修改成功', message: '密码已更新', type: 'success' });
|
||
} else {
|
||
showPrompt({ title: '修改失败', message: '请检查当前密码后重试', type: 'error' });
|
||
}
|
||
} catch (error: any) {
|
||
showPrompt({ title: '修改失败', message: resolveAuthApiError(error, '请稍后重试'), type: 'error' });
|
||
} finally {
|
||
setUpdatingPassword(false);
|
||
}
|
||
};
|
||
|
||
const handleSendChangePasswordCode = async () => {
|
||
if (changePwdCountdown > 0) {
|
||
return;
|
||
}
|
||
setSendingChangePwdCode(true);
|
||
try {
|
||
const ok = await authService.sendChangePasswordCode();
|
||
if (ok) {
|
||
setChangePwdCountdown(60);
|
||
showPrompt({ title: '发送成功', message: '改密验证码已发送到你的邮箱', type: 'success' });
|
||
}
|
||
} catch (error: any) {
|
||
showPrompt({ title: '发送失败', message: resolveAuthApiError(error, '验证码发送失败'), type: 'error' });
|
||
} finally {
|
||
setSendingChangePwdCode(false);
|
||
}
|
||
};
|
||
|
||
const content = (
|
||
<>
|
||
<View style={styles.section}>
|
||
<View style={styles.sectionHeader}>
|
||
<Text variant="caption" style={styles.sectionTitle}>
|
||
邮箱验证
|
||
</Text>
|
||
</View>
|
||
<View style={styles.card}>
|
||
<View style={styles.statusRow}>
|
||
<Text variant="body" color={colors.text.primary}>当前状态</Text>
|
||
<View style={[styles.statusBadge, isEmailVerified ? styles.statusVerified : styles.statusUnverified]}>
|
||
<Text variant="caption" color={isEmailVerified ? '#1B5E20' : '#B26A00'}>
|
||
{emailStatusText}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
|
||
<View style={styles.inputWrapper}>
|
||
<MaterialCommunityIcons name="email-outline" size={20} color={colors.text.secondary} style={styles.inputIcon} />
|
||
<TextInput
|
||
style={styles.input}
|
||
placeholder="请输入邮箱"
|
||
placeholderTextColor={colors.text.hint}
|
||
value={email}
|
||
onChangeText={setEmail}
|
||
autoCapitalize="none"
|
||
keyboardType="email-address"
|
||
/>
|
||
</View>
|
||
|
||
<View style={styles.codeRow}>
|
||
<View style={[styles.inputWrapper, styles.codeInput]}>
|
||
<MaterialCommunityIcons name="shield-key-outline" size={20} color={colors.text.secondary} style={styles.inputIcon} />
|
||
<TextInput
|
||
style={styles.input}
|
||
placeholder="验证码"
|
||
placeholderTextColor={colors.text.hint}
|
||
value={verificationCode}
|
||
onChangeText={setVerificationCode}
|
||
keyboardType="number-pad"
|
||
maxLength={6}
|
||
/>
|
||
</View>
|
||
<TouchableOpacity
|
||
style={[styles.sendCodeButton, (sendingCode || countdown > 0) && styles.buttonDisabled]}
|
||
onPress={handleSendCode}
|
||
disabled={sendingCode || countdown > 0}
|
||
>
|
||
{sendingCode ? (
|
||
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||
) : (
|
||
<Text style={styles.sendCodeButtonText}>{countdown > 0 ? `${countdown}s` : '发送验证码'}</Text>
|
||
)}
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
<TouchableOpacity
|
||
style={[styles.primaryButton, verifyingEmail && styles.buttonDisabled]}
|
||
onPress={handleVerifyEmail}
|
||
disabled={verifyingEmail}
|
||
>
|
||
{verifyingEmail ? (
|
||
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||
) : (
|
||
<Text style={styles.primaryButtonText}>验证邮箱</Text>
|
||
)}
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
|
||
<View style={styles.section}>
|
||
<View style={styles.sectionHeader}>
|
||
<Text variant="caption" style={styles.sectionTitle}>
|
||
修改密码
|
||
</Text>
|
||
</View>
|
||
<View style={styles.card}>
|
||
<View style={styles.inputWrapper}>
|
||
<MaterialCommunityIcons name="lock-outline" size={20} color={colors.text.secondary} style={styles.inputIcon} />
|
||
<TextInput
|
||
style={styles.input}
|
||
placeholder="当前密码"
|
||
placeholderTextColor={colors.text.hint}
|
||
value={oldPassword}
|
||
onChangeText={setOldPassword}
|
||
secureTextEntry
|
||
/>
|
||
</View>
|
||
<View style={styles.inputWrapper}>
|
||
<MaterialCommunityIcons name="lock-plus-outline" size={20} color={colors.text.secondary} style={styles.inputIcon} />
|
||
<TextInput
|
||
style={styles.input}
|
||
placeholder="新密码(至少6位)"
|
||
placeholderTextColor={colors.text.hint}
|
||
value={newPassword}
|
||
onChangeText={setNewPassword}
|
||
secureTextEntry
|
||
/>
|
||
</View>
|
||
<View style={styles.codeRow}>
|
||
<View style={[styles.inputWrapper, styles.codeInput]}>
|
||
<MaterialCommunityIcons name="shield-key-outline" size={20} color={colors.text.secondary} style={styles.inputIcon} />
|
||
<TextInput
|
||
style={styles.input}
|
||
placeholder="邮箱验证码"
|
||
placeholderTextColor={colors.text.hint}
|
||
value={changePasswordCode}
|
||
onChangeText={setChangePasswordCode}
|
||
keyboardType="number-pad"
|
||
maxLength={6}
|
||
/>
|
||
</View>
|
||
<TouchableOpacity
|
||
style={[styles.sendCodeButton, (sendingChangePwdCode || changePwdCountdown > 0) && styles.buttonDisabled]}
|
||
onPress={handleSendChangePasswordCode}
|
||
disabled={sendingChangePwdCode || changePwdCountdown > 0}
|
||
>
|
||
{sendingChangePwdCode ? (
|
||
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||
) : (
|
||
<Text style={styles.sendCodeButtonText}>
|
||
{changePwdCountdown > 0 ? `${changePwdCountdown}s` : '发送验证码'}
|
||
</Text>
|
||
)}
|
||
</TouchableOpacity>
|
||
</View>
|
||
<View style={styles.inputWrapper}>
|
||
<MaterialCommunityIcons name="lock-check-outline" size={20} color={colors.text.secondary} style={styles.inputIcon} />
|
||
<TextInput
|
||
style={styles.input}
|
||
placeholder="确认新密码"
|
||
placeholderTextColor={colors.text.hint}
|
||
value={confirmPassword}
|
||
onChangeText={setConfirmPassword}
|
||
secureTextEntry
|
||
/>
|
||
</View>
|
||
|
||
<TouchableOpacity
|
||
style={[styles.primaryButton, updatingPassword && styles.buttonDisabled]}
|
||
onPress={handleChangePassword}
|
||
disabled={updatingPassword}
|
||
>
|
||
{updatingPassword ? (
|
||
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||
) : (
|
||
<Text style={styles.primaryButtonText}>更新密码</Text>
|
||
)}
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
</>
|
||
);
|
||
|
||
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
|
||
|
||
return (
|
||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
||
<ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}>
|
||
{content}
|
||
</ScrollView>
|
||
</SafeAreaView>
|
||
);
|
||
};
|
||
|
||
function createAccountSecurityStyles(colors: AppColors) {
|
||
return StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: colors.background.paper,
|
||
},
|
||
scrollContent: {
|
||
paddingVertical: spacing.lg,
|
||
},
|
||
section: {
|
||
marginBottom: spacing['2xl'],
|
||
},
|
||
sectionHeader: {
|
||
paddingHorizontal: spacing['2xl'],
|
||
marginBottom: spacing.sm,
|
||
marginTop: spacing.sm,
|
||
},
|
||
sectionTitle: {
|
||
fontWeight: '600',
|
||
fontSize: fontSizes.sm,
|
||
color: colors.text.secondary,
|
||
textTransform: 'uppercase',
|
||
letterSpacing: 0.5,
|
||
},
|
||
card: {
|
||
marginHorizontal: spacing['2xl'],
|
||
maxWidth: CONTENT_MAX_WIDTH,
|
||
alignSelf: 'center',
|
||
width: '100%',
|
||
},
|
||
statusRow: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
marginBottom: spacing.md,
|
||
},
|
||
statusBadge: {
|
||
paddingHorizontal: spacing.sm,
|
||
paddingVertical: 4,
|
||
borderRadius: borderRadius.sm,
|
||
},
|
||
statusVerified: {
|
||
backgroundColor: colors.success.light + '40',
|
||
},
|
||
statusUnverified: {
|
||
backgroundColor: colors.warning.light + '40',
|
||
},
|
||
statusVerifiedText: {
|
||
color: colors.success.dark,
|
||
},
|
||
statusUnverifiedText: {
|
||
color: colors.warning.dark,
|
||
},
|
||
inputWrapper: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
backgroundColor: colors.background.default,
|
||
borderRadius: 14,
|
||
paddingHorizontal: spacing.md,
|
||
height: 50,
|
||
marginBottom: spacing.sm,
|
||
},
|
||
inputIcon: {
|
||
marginRight: spacing.sm,
|
||
},
|
||
input: {
|
||
flex: 1,
|
||
color: colors.text.primary,
|
||
fontSize: fontSizes.md,
|
||
height: 50,
|
||
},
|
||
codeRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: spacing.sm,
|
||
marginBottom: spacing.sm,
|
||
},
|
||
codeInput: {
|
||
flex: 1,
|
||
marginBottom: 0,
|
||
},
|
||
sendCodeButton: {
|
||
height: 50,
|
||
minWidth: 110,
|
||
borderRadius: 14,
|
||
backgroundColor: colors.primary.main,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
paddingHorizontal: spacing.sm,
|
||
},
|
||
sendCodeButtonText: {
|
||
color: colors.text.inverse,
|
||
fontSize: fontSizes.sm,
|
||
fontWeight: '600',
|
||
},
|
||
primaryButton: {
|
||
height: 50,
|
||
borderRadius: 14,
|
||
backgroundColor: colors.primary.main,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
marginTop: spacing.xs,
|
||
},
|
||
primaryButtonText: {
|
||
color: colors.text.inverse,
|
||
fontSize: fontSizes.md,
|
||
fontWeight: '600',
|
||
},
|
||
buttonDisabled: {
|
||
opacity: 0.6,
|
||
},
|
||
});
|
||
}
|
||
|
||
export default AccountSecurityScreen;
|