feat(auth): add user identity verification system with new screens
Add comprehensive identity verification flow including: - New verification guide screen at /verification-guide for selecting identity type - New verification form screen at /verification-form for submitting verification documents - Verification settings screen at /profile/verification for managing verification status - Move terms and privacy policy to root-level routes (/terms, /privacy) - Update login to require active terms agreement checkbox - Database: add in-memory fallback when OPFS is unavailable on web BREAKING CHANGE: Terms and privacy policy routes moved from /profile/terms and /profile/privacy to /terms and /privacy
This commit is contained in:
@@ -40,6 +40,7 @@ export default function ProfileStackLayout() {
|
||||
<Stack.Screen name="about" options={{ title: '关于我们' }} />
|
||||
<Stack.Screen name="terms" options={{ title: '用户协议' }} />
|
||||
<Stack.Screen name="privacy" options={{ title: '隐私政策' }} />
|
||||
<Stack.Screen name="verification" options={{ title: '身份认证' }} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
5
app/(app)/(tabs)/profile/verification.tsx
Normal file
5
app/(app)/(tabs)/profile/verification.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { VerificationSettingsScreen } from '@/screens/profile/VerificationSettingsScreen';
|
||||
|
||||
export default function VerificationSettingsPage() {
|
||||
return <VerificationSettingsScreen />;
|
||||
}
|
||||
5
app/(auth)/verification-form.tsx
Normal file
5
app/(auth)/verification-form.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { VerificationFormScreen } from '@/screens/auth/VerificationFormScreen';
|
||||
|
||||
export default function VerificationFormPage() {
|
||||
return <VerificationFormScreen />;
|
||||
}
|
||||
5
app/(auth)/verification-guide.tsx
Normal file
5
app/(auth)/verification-guide.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { VerificationGuideScreen } from '@/screens/auth/VerificationGuideScreen';
|
||||
|
||||
export default function VerificationGuidePage() {
|
||||
return <VerificationGuideScreen />;
|
||||
}
|
||||
@@ -175,6 +175,30 @@ function ThemedStack() {
|
||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(app)" options={{ headerShown: false }} />
|
||||
<Stack.Screen
|
||||
name="terms"
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: '用户协议',
|
||||
headerStyle: { backgroundColor: colors.background.paper },
|
||||
headerTintColor: colors.text.primary,
|
||||
headerShadowVisible: false,
|
||||
headerBackVisible: false,
|
||||
headerLeft: () => <AppBackButton onPress={() => router.back()} />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="privacy"
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: '隐私政策',
|
||||
headerStyle: { backgroundColor: colors.background.paper },
|
||||
headerTintColor: colors.text.primary,
|
||||
headerShadowVisible: false,
|
||||
headerBackVisible: false,
|
||||
headerLeft: () => <AppBackButton onPress={() => router.back()} />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="post/[postId]"
|
||||
options={{
|
||||
|
||||
5
app/privacy.tsx
Normal file
5
app/privacy.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { PrivacyPolicyScreen } from '../src/screens/profile/PrivacyPolicyScreen';
|
||||
|
||||
export default function PrivacyRoute() {
|
||||
return <PrivacyPolicyScreen />;
|
||||
}
|
||||
5
app/terms.tsx
Normal file
5
app/terms.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { TermsOfServiceScreen } from '../src/screens/profile/TermsOfServiceScreen';
|
||||
|
||||
export default function TermsRoute() {
|
||||
return <TermsOfServiceScreen />;
|
||||
}
|
||||
@@ -9,6 +9,6 @@ export const DB_CONFIG = {
|
||||
DB_NAME_PREFIX: 'carrot_bbs_',
|
||||
} as const;
|
||||
|
||||
export const getDbName = (userId: string | null): string => {
|
||||
export function getDbName(userId: string | null): string {
|
||||
return userId ? `${DB_CONFIG.DB_NAME_PREFIX}${userId}.db` : DB_CONFIG.DEFAULT_DB_NAME;
|
||||
};
|
||||
}
|
||||
@@ -109,25 +109,31 @@ class DatabaseManager {
|
||||
const isWeb = typeof window !== 'undefined';
|
||||
|
||||
return LockManager.withWebLock(`${DB_CONFIG.LOCK_PREFIX}${dbName}`, async () => {
|
||||
let lastError: unknown;
|
||||
for (let attempt = 0; attempt <= DB_CONFIG.MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
const database = await SQLite.openDatabaseAsync(dbName);
|
||||
console.log(`[DatabaseManager] 成功打开数据库 (尝试 ${attempt + 1}/${DB_CONFIG.MAX_RETRIES + 1})`);
|
||||
return database;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
const msg = String(error);
|
||||
console.error(`[DatabaseManager] 打开失败 (尝试 ${attempt + 1}/${DB_CONFIG.MAX_RETRIES + 1}):`, msg);
|
||||
|
||||
if (isWeb && msg.includes('cannot create file')) {
|
||||
console.log('[DatabaseManager] OPFS 不可用,回退到内存数据库');
|
||||
const memoryDb = await SQLite.openDatabaseAsync(':memory:');
|
||||
return memoryDb;
|
||||
}
|
||||
|
||||
if (attempt < DB_CONFIG.MAX_RETRIES) {
|
||||
const delay = isWeb ? DB_CONFIG.WEB_RETRY_DELAY_BASE * (attempt + 1) : DB_CONFIG.NATIVE_RETRY_DELAY;
|
||||
console.warn(`[DatabaseManager] ${delay}ms 后重试...`);
|
||||
await new Promise(r => setTimeout(r, delay));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error('打开数据库失败');
|
||||
throw lastError;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -185,9 +185,24 @@ export function hrefProfileAbout(): string {
|
||||
}
|
||||
|
||||
export function hrefProfileTerms(): string {
|
||||
return '/profile/terms';
|
||||
return '/terms';
|
||||
}
|
||||
|
||||
export function hrefProfilePrivacy(): string {
|
||||
return '/profile/privacy';
|
||||
return '/privacy';
|
||||
}
|
||||
|
||||
export function hrefProfileVerification(): string {
|
||||
return '/profile/verification';
|
||||
}
|
||||
|
||||
export function hrefVerificationGuide(): string {
|
||||
return '/verification-guide';
|
||||
}
|
||||
|
||||
export function hrefVerificationForm(identity?: string): string {
|
||||
if (identity) {
|
||||
return `/verification-form?identity=${encodeURIComponent(identity)}`;
|
||||
}
|
||||
return '/verification-form';
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ export const LoginScreen: React.FC = () => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [agreedToTerms, setAgreedToTerms] = useState(false);
|
||||
|
||||
// 动画值
|
||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||
@@ -84,6 +85,10 @@ export const LoginScreen: React.FC = () => {
|
||||
showPrompt({ title: '提示', message: '请输入密码', type: 'warning' });
|
||||
return false;
|
||||
}
|
||||
if (!agreedToTerms) {
|
||||
showPrompt({ title: '提示', message: '请同意用户协议和隐私政策', type: 'warning' });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -211,6 +216,37 @@ export const LoginScreen: React.FC = () => {
|
||||
<Text style={styles.forgotPasswordText}>忘记密码?</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 服务条款勾选 */}
|
||||
<View style={styles.termsContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={() => setAgreedToTerms(!agreedToTerms)}
|
||||
activeOpacity={0.8}
|
||||
style={styles.checkboxWrapper}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={agreedToTerms ? 'checkbox-marked' : 'checkbox-blank-outline'}
|
||||
size={22}
|
||||
color={agreedToTerms ? THEME_COLORS.primary : colors.text.hint}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.termsText}>
|
||||
我已阅读并同意
|
||||
<Text
|
||||
style={styles.termsLink}
|
||||
onPress={() => router.push(hrefs.hrefProfileTerms())}
|
||||
>
|
||||
《用户协议》
|
||||
</Text>
|
||||
和
|
||||
<Text
|
||||
style={styles.termsLink}
|
||||
onPress={() => router.push(hrefs.hrefProfilePrivacy())}
|
||||
>
|
||||
《隐私政策》
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 登录按钮 */}
|
||||
<TouchableOpacity
|
||||
style={[styles.loginButton, loading && styles.loginButtonDisabled]}
|
||||
@@ -234,15 +270,7 @@ export const LoginScreen: React.FC = () => {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 协议提示 */}
|
||||
<View style={styles.policyFooter}>
|
||||
<Text style={styles.policyText}>
|
||||
登录即表示您同意
|
||||
<Text style={styles.policyLink} onPress={() => router.push(hrefs.hrefProfileTerms())}>《用户协议》</Text>
|
||||
和
|
||||
<Text style={styles.policyLink} onPress={() => router.push(hrefs.hrefProfilePrivacy())}>《隐私政策》</Text>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
</Animated.View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
@@ -394,6 +422,26 @@ function createLoginStyles(colors: AppColors) {
|
||||
color: THEME_COLORS.primary,
|
||||
fontWeight: '500',
|
||||
},
|
||||
termsContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 20,
|
||||
marginTop: 4,
|
||||
},
|
||||
checkboxWrapper: {
|
||||
paddingTop: 2,
|
||||
},
|
||||
termsText: {
|
||||
fontSize: 14,
|
||||
color: colors.text.secondary,
|
||||
marginLeft: 10,
|
||||
flex: 1,
|
||||
lineHeight: 22,
|
||||
},
|
||||
termsLink: {
|
||||
color: THEME_COLORS.primary,
|
||||
fontWeight: '500',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export const RegisterStep3Profile: React.FC<RegisterStepProps> = ({
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [agreedToTerms, setAgreedToTerms] = useState(true);
|
||||
const [agreedToTerms, setAgreedToTerms] = useState(false);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
|
||||
@@ -37,7 +37,7 @@ const IDENTITY_OPTIONS = [
|
||||
key: 'teacher',
|
||||
title: '教职工',
|
||||
description: '需要提交工作证或教师资格证',
|
||||
icon: 'chalkboard-teacher',
|
||||
icon: 'account-tie',
|
||||
},
|
||||
{
|
||||
key: 'staff',
|
||||
@@ -82,10 +82,7 @@ export const VerificationGuideScreen: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
// 导航到认证表单页面
|
||||
router.push({
|
||||
pathname: '/auth/verification-form',
|
||||
params: { identity: selectedIdentity },
|
||||
});
|
||||
router.push(hrefs.hrefVerificationForm(selectedIdentity));
|
||||
};
|
||||
|
||||
const renderIdentityCard = (option: typeof IDENTITY_OPTIONS[0]) => {
|
||||
@@ -148,7 +145,7 @@ export const VerificationGuideScreen: React.FC = () => {
|
||||
<View style={styles.benefitsContainer}>
|
||||
<View style={styles.benefitItem}>
|
||||
<MaterialCommunityIcons
|
||||
name="verified"
|
||||
name="check-decagram"
|
||||
size={20}
|
||||
color={THEME_COLORS.primary}
|
||||
/>
|
||||
@@ -164,7 +161,7 @@ export const VerificationGuideScreen: React.FC = () => {
|
||||
</View>
|
||||
<View style={styles.benefitItem}>
|
||||
<MaterialCommunityIcons
|
||||
name="account-group"
|
||||
name="account-group-outline"
|
||||
size={20}
|
||||
color={THEME_COLORS.primary}
|
||||
/>
|
||||
|
||||
@@ -7,6 +7,8 @@ export { LoginScreen } from './LoginScreen';
|
||||
export { RegisterScreen } from './RegisterScreen';
|
||||
export { ForgotPasswordScreen } from './ForgotPasswordScreen';
|
||||
export { WelcomeScreen } from './WelcomeScreen';
|
||||
export { VerificationGuideScreen } from './VerificationGuideScreen';
|
||||
export { VerificationFormScreen } from './VerificationFormScreen';
|
||||
|
||||
// 注册步骤组件
|
||||
export { RegisterStep1Email } from './RegisterStep1Email';
|
||||
|
||||
@@ -254,7 +254,7 @@ export const SettingsScreen: React.FC = () => {
|
||||
router.push(hrefs.hrefProfileEdit());
|
||||
break;
|
||||
case 'verification':
|
||||
router.push('/profile/verification');
|
||||
router.push(hrefs.hrefProfileVerification());
|
||||
break;
|
||||
case 'notification_settings':
|
||||
router.push(hrefs.hrefProfileNotifications());
|
||||
|
||||
@@ -84,7 +84,7 @@ export const VerificationSettingsScreen: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
const handleStartVerification = () => {
|
||||
router.push('/auth/verification-guide');
|
||||
router.push(hrefs.hrefVerificationGuide());
|
||||
};
|
||||
|
||||
const handleViewRecords = () => {
|
||||
@@ -109,27 +109,10 @@ export const VerificationSettingsScreen: React.FC = () => {
|
||||
const identityText = IDENTITY_TEXT[status?.identity || 'general'];
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => router.back()}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name="arrow-left"
|
||||
size={24}
|
||||
color={colors.text?.primary || '#333'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>身份认证</Text>
|
||||
<View style={styles.headerRight} />
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* 状态卡片 */}
|
||||
<View style={styles.statusCard}>
|
||||
<View style={[styles.statusIconContainer, { backgroundColor: statusConfig.color + '20' }]}>
|
||||
@@ -167,7 +150,7 @@ export const VerificationSettingsScreen: React.FC = () => {
|
||||
<View style={styles.benefitItem}>
|
||||
<View style={styles.benefitIconContainer}>
|
||||
<MaterialCommunityIcons
|
||||
name="verified"
|
||||
name="check-decagram"
|
||||
size={24}
|
||||
color={THEME_COLORS.primary}
|
||||
/>
|
||||
@@ -183,7 +166,7 @@ export const VerificationSettingsScreen: React.FC = () => {
|
||||
<View style={styles.benefitItem}>
|
||||
<View style={styles.benefitIconContainer}>
|
||||
<MaterialCommunityIcons
|
||||
name="lock-open-variant"
|
||||
name="lock-open-outline"
|
||||
size={24}
|
||||
color={THEME_COLORS.primary}
|
||||
/>
|
||||
@@ -199,7 +182,7 @@ export const VerificationSettingsScreen: React.FC = () => {
|
||||
<View style={styles.benefitItem}>
|
||||
<View style={styles.benefitIconContainer}>
|
||||
<MaterialCommunityIcons
|
||||
name="account-group"
|
||||
name="account-group-outline"
|
||||
size={24}
|
||||
color={THEME_COLORS.primary}
|
||||
/>
|
||||
@@ -278,7 +261,6 @@ export const VerificationSettingsScreen: React.FC = () => {
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export { AccountSecurityScreen } from './AccountSecurityScreen';
|
||||
export { AboutScreen } from './AboutScreen';
|
||||
export { TermsOfServiceScreen } from './TermsOfServiceScreen';
|
||||
export { PrivacyPolicyScreen } from './PrivacyPolicyScreen';
|
||||
export { VerificationSettingsScreen } from './VerificationSettingsScreen';
|
||||
|
||||
// 导出 Hook 供需要自定义的场景使用
|
||||
export { useUserProfile, createSharedProfileStyles, TABS, TAB_ICONS } from './useUserProfile';
|
||||
|
||||
Reference in New Issue
Block a user