import { Ionicons } from '@expo/vector-icons' import { Block, Text, Toast } from '@share/components' import { router } from 'expo-router' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { TextInput } from 'react-native' import { KeyboardAwareScrollView } from 'react-native-keyboard-controller' import { APP_VERSION } from '@/app.config' import BannerSection from '@/components/BannerSection' import { getSession, phoneNumber, setAuthToken, signIn } from '@/lib/auth' import { isValidPhone } from '@/utils' import { openUrl } from '@/utils/webview-helper' const APP_NAME = '多米' type LoginType = 'phone' | 'email' // 判断是否为邮箱格式的辅助函数 const isEmail = (input: string) => { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input) } export default function Auth() { const [loginType, setLoginType] = useState('phone') const [phone, setPhone] = useState('') const [code, setCode] = useState('') const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [showPassword, setShowPassword] = useState(false) const [loading, setLoading] = useState(false) const [countdown, setCountdown] = useState(0) const [agreed, setAgreed] = useState(false) const countdownTimerRef = useRef | null>(null) const canSendCode = useMemo(() => { return isValidPhone(phone) && countdown === 0 }, [phone, countdown]) // 清理倒计时定时器 useEffect(() => { return () => { if (countdownTimerRef.current) { clearInterval(countdownTimerRef.current) } } }, []) // 获取验证码 const handleSendCode = useCallback(async () => { if (!isValidPhone(phone)) { Toast.show({ title: '请输入有效的手机号' }) return } if (countdown > 0) { return } Toast.showLoading({ title: '正在获取验证码...' }) setLoading(true) try { const result = await phoneNumber.sendOtp({ phoneNumber: phone, }) if (result.error) { // 处理不同类型的错误 const errorMessage = result.error.message || '验证码发送失败' // 如果是 403 错误,可能是请求过于频繁 if (result.error.status === 403) { Toast.show({ title: '请求过于频繁,请稍后再试' }) } else { Toast.show({ title: errorMessage }) } return } setCountdown(60) // 开始倒计时 Toast.show({ title: '验证码已发送,请查收短信' }) // 倒计时逻辑 if (countdownTimerRef.current) { clearInterval(countdownTimerRef.current) } countdownTimerRef.current = setInterval(() => { setCountdown((prev) => { if (prev <= 1) { if (countdownTimerRef.current) { clearInterval(countdownTimerRef.current) countdownTimerRef.current = null } return 0 } return prev - 1 }) }, 1000) } catch (error: any) { console.error('获取验证码失败:', error) Toast.show({ title: error.message || '验证码发送失败,请稍后重试' }) } finally { Toast.hideLoading() setLoading(false) } }, [phone, countdown]) // 手机号验证码登录/注册(无需密码) const handlePhoneLogin = useCallback(async () => { if (!phone || !code) { Toast.show({ title: '请填写手机号和验证码' }) return } if (!isValidPhone(phone)) { Toast.show({ title: '请输入有效的手机号' }) return } if (code.length !== 6) { Toast.show({ title: '请输入6位验证码' }) return } if (!agreed) { Toast.show({ title: '请先阅读并同意服务条款和隐私协议' }) return } setLoading(true) Toast.showLoading({ title: '正在验证...' }) try { // 验证验证码,如果后端配置了 signUpOnVerification,新用户会自动注册并创建 session // disableSession: false 表示验证成功后创建 session(自动登录) await new Promise((resolve, reject) => { phoneNumber.verify( { phoneNumber: phone, code: code, disableSession: false }, { onSuccess: async (ctx: any) => { const authToken = ctx.response.headers.get('set-auth-token') if (authToken) { setAuthToken(authToken) } await getSession() Toast.show({ title: '登录成功!' }) setTimeout(() => { router.replace('/(tabs)') }, 500) }, onError: (ctx: any) => { reject(new Error(ctx.error.message)) }, }, ) }) } catch (error: any) { console.error('登录/注册失败:', error) Toast.show({ title: error.message || '操作失败,请稍后重试' }) } finally { Toast.hideLoading() setLoading(false) } }, [phone, code, agreed]) // 邮箱登录 const handleEmailLogin = useCallback(async () => { if (!username || !password) { Toast.show({ title: '请填写账号和密码' }) return } if (!agreed) { Toast.show({ title: '请先阅读并同意服务条款和隐私协议' }) return } setLoading(true) Toast.showLoading({ title: '正在登录...' }) try { let result if (isEmail(username)) { // 如果用户名是邮箱格式,使用邮箱登录 result = await signIn.email( { email: username, password, }, { onSuccess: async (ctx) => { const authToken = ctx.response.headers.get('set-auth-token') if (authToken) { setAuthToken(authToken) } }, onError: (ctx) => { console.error(`[LOGIN] email login error`, ctx) }, }, ) } else { // 如果用户名不是邮箱格式,使用用户名登录 result = await signIn.username( { username, password, }, { onSuccess: async (ctx) => { const authToken = ctx.response.headers.get('set-auth-token') if (authToken) { setAuthToken(authToken) } }, onError: (ctx) => { console.error(`[LOGIN] username login error`, ctx) }, }, ) } if (result.error) { Toast.show({ title: '请输入正确的账号或密码' }) } else { Toast.show({ title: '登录成功!' }) router.replace('/(tabs)') } } catch (error: any) { Toast.show({ title: error.message || '登录失败' }) } finally { Toast.hideLoading() setLoading(false) } }, [username, password, agreed]) const handleLogin = loginType === 'phone' ? handlePhoneLogin : handleEmailLogin // 服务条款同意组件 const renderAgreementCheckbox = () => ( setAgreed(!agreed)} className={`size-[20px] items-center justify-center border-2 border-black ${agreed ? 'bg-black' : 'bg-white'}`} > {agreed && } 已阅读并同意 router.push('/service')} > 服务条款 router.push('/privacy')} > 隐私协议 ) // 登录按钮组件 const renderLoginButton = (buttonText: string = '登录') => ( {loading ? ( ) : ( )} {loading ? '处理中...' : buttonText} ) // 登录方式切换组件 const renderLoginTypeSwitch = () => ( {(['phone', 'email'] as const).map((type) => { const isActive = type === loginType return ( setLoginType(type)}> ) })} ) return ( {APP_NAME} {loginType === 'phone' ? ( <> 手机号 0 ? undefined : handleSendCode} className={`border-2 border-black px-[6px] py-[4px] ${canSendCode && countdown === 0 ? 'bg-black' : 'bg-gray-200'}`} > {countdown > 0 ? `${countdown}秒` : '获取验证码'} 验证码 {renderAgreementCheckbox()} {renderLoginButton('登录/注册')} ) : ( <> 账号 密码 setShowPassword(!showPassword)} className="p-1"> {renderAgreementCheckbox()} router.push('/forgotPassword')} > 忘记密码 {renderLoginButton('登录')} )} {renderLoginTypeSwitch()} © 2025 LOOMART. All rights reserved. 当前版本号: {APP_VERSION} ) }