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; // 胡萝卜橙主题色 const THEME_COLORS = { primary: '#FF6B35', primaryLight: '#FF8C5A', primaryDark: '#E55A2B', }; 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 < 8) { showPrompt({ title: '提示', message: '新密码至少 8 位', type: 'warning' }); return; } if (!/[A-Z]/.test(newPassword)) { showPrompt({ title: '提示', message: '新密码必须包含大写字母', type: 'warning' }); return; } if (!/[a-z]/.test(newPassword)) { showPrompt({ title: '提示', message: '新密码必须包含小写字母', type: 'warning' }); return; } if (!/[0-9]/.test(newPassword)) { showPrompt({ title: '提示', message: '新密码必须包含数字', 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 responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 }); return ( {/* 邮箱验证分组 */} 邮箱验证 {/* 状态显示 */} 当前状态 {emailStatusText} {/* 邮箱输入框 */} {/* 验证码行 */} 0) && styles.buttonDisabled]} onPress={handleSendCode} disabled={sendingCode || countdown > 0} > {sendingCode ? ( ) : ( {countdown > 0 ? `${countdown}s` : '发送验证码'} )} {/* 验证按钮 */} {verifyingEmail ? ( ) : ( 验证邮箱 )} {/* 修改密码分组 */} 修改密码 {/* 当前密码 */} {/* 新密码 */} {/* 确认新密码 */} {/* 验证码行 */} 0) && styles.buttonDisabled]} onPress={handleSendChangePasswordCode} disabled={sendingChangePwdCode || changePwdCountdown > 0} > {sendingChangePwdCode ? ( ) : ( {changePwdCountdown > 0 ? `${changePwdCountdown}s` : '发送验证码'} )} {/* 更新密码按钮 */} {updatingPassword ? ( ) : ( 更新密码 )} ); }; function createAccountSecurityStyles(colors: AppColors) { return StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', }, scrollContent: { paddingVertical: spacing.lg, }, content: { maxWidth: CONTENT_MAX_WIDTH, alignSelf: 'center', width: '100%', }, // 分组标题 groupHeader: { paddingHorizontal: spacing['2xl'], marginBottom: spacing.sm, marginTop: spacing.sm, }, groupTitle: { fontWeight: '600', fontSize: fontSizes.sm, color: colors.text.secondary, textTransform: 'uppercase', letterSpacing: 0.5, }, // 卡片样式 - 统一 card: { backgroundColor: '#F5F5F7', borderRadius: 16, padding: 6, marginBottom: spacing['2xl'], marginHorizontal: spacing['2xl'], }, // 状态显示 statusRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: spacing.md, paddingVertical: spacing.md, marginBottom: spacing.md, backgroundColor: '#fff', borderRadius: 12, }, statusLabel: { fontSize: 15, color: colors.text.secondary, }, statusBadge: { paddingHorizontal: spacing.sm, paddingVertical: 4, borderRadius: borderRadius.sm, }, statusVerified: { backgroundColor: '#E8F5E9', }, statusUnverified: { backgroundColor: '#FFF3E0', }, statusText: { fontSize: 13, fontWeight: '600', }, statusVerifiedText: { color: '#2E7D32', }, statusUnverifiedText: { color: '#E65100', }, // 输入框样式 - 统一 inputWrapper: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#fff', borderRadius: 14, paddingHorizontal: spacing.md, height: 56, marginBottom: spacing.sm, }, inputIcon: { marginRight: spacing.sm, }, input: { flex: 1, color: colors.text.primary, fontSize: fontSizes.md, height: 56, }, // 验证码行 codeRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm, marginBottom: spacing.sm, }, codeInput: { flex: 1, marginBottom: 0, }, // 发送验证码按钮 sendCodeButton: { height: 56, minWidth: 110, borderRadius: 14, backgroundColor: THEME_COLORS.primary, alignItems: 'center', justifyContent: 'center', paddingHorizontal: spacing.sm, }, sendCodeButtonText: { color: '#fff', fontSize: fontSizes.sm, fontWeight: '600', }, // 主按钮样式 - 统一 primaryButton: { height: 56, borderRadius: 14, backgroundColor: THEME_COLORS.primary, alignItems: 'center', justifyContent: 'center', marginTop: spacing.xs, }, primaryButtonText: { color: '#fff', fontSize: fontSizes.md, fontWeight: '600', }, buttonDisabled: { opacity: 0.6, }, }); } export default AccountSecurityScreen;