From 18dc038c52d8c25ab30f3fe846a5158af25b7f73 Mon Sep 17 00:00:00 2001 From: gww Date: Mon, 26 Jan 2026 13:56:55 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=89=8B=E6=9C=BA=E5=8F=B7=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E7=A0=81=E5=8F=91=E9=80=81=E6=8E=A5=E5=8F=A3=E8=BF=98?= =?UTF-8?q?=E6=9C=AA=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/auth.tsx | 556 ++++++++++++------------------------------------- lib/auth.ts | 19 +- utils/index.ts | 6 + 3 files changed, 160 insertions(+), 421 deletions(-) diff --git a/app/auth.tsx b/app/auth.tsx index da405dc..7de89ec 100644 --- a/app/auth.tsx +++ b/app/auth.tsx @@ -7,425 +7,104 @@ import { KeyboardAwareScrollView } from 'react-native-keyboard-controller' import { APP_VERSION } from '@/app.config' import BannerSection from '@/components/BannerSection' -import { emailOtp as emailOtpService, setAuthToken, signIn, signUp } from '@/lib/auth' -import { isValidEmail } from '@/utils' -type AuthMode = 'login' | 'register' | 'registerEmail' +import { phoneNumber, setAuthToken } from '@/lib/auth' +import { isValidPhone } from '@/utils' const APP_NAME = '多米' -// 判断是否为邮箱格式的辅助函数 -const isEmail = (input: string) => { - return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input) -} - export default function Auth() { - const [mode, setMode] = useState('login') - - const [username, setUsername] = useState('') - const [email, setEmail] = useState('') - const [emailOtp, setEmailOtp] = useState('') - const [password, setPassword] = useState('') - const [confirmPassword, setConfirmPassword] = useState('') - const [showPassword, setShowPassword] = useState(false) - const [showConfirmPassword, setShowConfirmPassword] = useState(false) + const [phone, setPhone] = useState('') + const [code, setCode] = useState('') const [loading, setLoading] = useState(false) + const [countdown, setCountdown] = useState(0) - const canSend = useMemo(() => { - return isValidEmail(email) - }, [email]) + const canSendCode = useMemo(() => { + return isValidPhone(phone) + }, [phone]) - const handleEmiallOtp = () => { - if (!canSend) { - Toast.show({ title: '请输入有效的邮箱地址' }) + // TODO: 获取验证码接口还未实现,需要后端提供手机号验证码发送接口 + const handleSendCode = useCallback(async () => { + if (!canSendCode) { + Toast.show({ title: '请输入有效的手机号' }) return } Toast.showLoading({ title: '正在发送验证码...' }) setLoading(true) - emailOtpService - .sendVerificationOtp({ email, type: 'email-verification' }) - .then((res) => { - if (res.error) { - Toast.show({ title: res.error.message || '验证码发送失败' }) - } else { - Toast.show({ title: '验证码已发送,请查收邮箱' }) - } - }) - .finally(() => { - Toast.hideLoading() - setLoading(false) - }) - } - const handleEmailVerify = async () => { - if (!canSend) { - Toast.show({ title: '请输入有效的邮箱地址' }) - return - } - if (!emailOtp) { - Toast.show({ title: '请输入邮箱验证码' }) - return - } - try { - Toast.showLoading({ title: '正在验证' }) - const emailVerifyRes = await emailOtpService.verifyEmail({ email, otp: emailOtp }) - Toast.hideLoading() - console.log('emailVerifyRes------------', emailVerifyRes) - if (emailVerifyRes?.error) { - Toast.show({ title: '邮箱验证码验证失败' }) + // TODO: 调用获取验证码接口 + // 当前使用 better-auth 的 phoneNumber.sendOtp,但后端可能还未实现 + // 如果后端未实现,这里会报错,需要后端提供手机号验证码发送接口 + const result = await phoneNumber.sendOtp({ + phoneNumber: phone, + }) + + if (result.error) { + Toast.show({ title: result.error.message || '验证码发送失败,请检查后端接口是否已实现' }) return } - setMode('register') - } catch (error) {} - } + + Toast.show({ title: '验证码已发送,请查收短信' }) + setCountdown(60) // 开始倒计时 + + // 倒计时逻辑 + const timer = setInterval(() => { + setCountdown((prev) => { + if (prev <= 1) { + clearInterval(timer) + return 0 + } + return prev - 1 + }) + }, 1000) + } catch (error: any) { + Toast.show({ title: error.message || '验证码发送失败' }) + } finally { + Toast.hideLoading() + setLoading(false) + } + }, [phone, canSendCode]) const handleLogin = useCallback(async () => { - if (!username || !password) { - Toast.show({ title: '请填写账号和密码' }) + if (!phone || !code) { + Toast.show({ title: '请填写手机号和验证码' }) + return + } + + if (!isValidPhone(phone)) { + Toast.show({ title: '请输入有效的手机号' }) return } setLoading(true) 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) - }, - }, - ) - } + // 使用 better-auth 的 phoneNumber.verify 进行验证和登录 + // verify 方法会自动创建会话,如果用户不存在会自动注册 + const result = await phoneNumber.verify({ + phoneNumber: phone, + code, + updatePhoneNumber: true, // 如果已登录,更新手机号 + }) if (result.error) { - Toast.show({ title: '请输入正确的账号或密码' }) - } else { - Toast.show({ title: '登录成功!' }) - router.replace('/(tabs)') + Toast.show({ title: result.error.message || '验证码错误或已过期' }) + return } + + // 获取认证 token(如果后端返回) + // better-auth 会自动处理 session,但如果有自定义 token,需要手动设置 + if (result.data && 'token' in result.data && result.data.token) { + await setAuthToken(result.data.token) + } + + Toast.show({ title: '登录成功!' }) + router.replace('/(tabs)') } catch (error: any) { Toast.show({ title: error.message || '登录失败' }) } finally { setLoading(false) } - }, [username, password, signIn]) - - const handleRegister = async () => { - if (!email || !username || !password || !emailOtp) { - Toast.show({ title: '请填写所有必填项' }) - return - } - - if (password !== confirmPassword) { - Toast.show({ title: '两次密码输入不一致' }) - return - } - - if (password.length < 6) { - Toast.show({ title: '密码长度至少6位' }) - return - } - - setLoading(true) - try { - const result = await signUp.email({ - email, - username, - password, - name: username, - }) - - // console.log('result------------', result) - - const messageMap = { - USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: '该邮箱已被注册,请使用其他邮箱', - USERNAME_ALREADY_EXISTS_USE_ANOTHER_USERNAME: '该用户名已被注册,请使用其他用户名', - } - const friendlyMessage = messageMap[result?.error?.code] - - if (result.error) { - Toast.show({ title: friendlyMessage || result?.error?.message || '注册失败' }) - } else { - Toast.show({ title: '注册成功!' }) - } - } catch (error: any) { - Toast.show({ title: error.message || '注册失败' }) - } finally { - setLoading(false) - } - } - - const renderLogin = () => { - if (mode !== 'login') { - return null - } - return ( - - - 账号 - - - - - - - 密码 - - - - setShowPassword(!showPassword)} className="p-1"> - - - - - - router.push('/forgotPassword')}> - 忘记密码 - - - - {loading ? ( - - ) : ( - - )} - {loading ? '处理中...' : '登录'} - - - ) - } - const renderEmailSend = () => { - if (mode !== 'registerEmail') { - return null - } - return ( - - - 邮箱 - - - - - 发送验证码 - - - - - - 邮箱验证码 - - - - - - - - {loading ? ( - - ) : ( - - )} - 下一步 - - - ) - } - - const renderRegister = () => { - if (mode !== 'register') { - return null - } - return ( - - - 账号 - - - - - - - 密码 - - - - setShowPassword(!showPassword)} className="p-1"> - - - - - - 确认密码 - - - - setShowConfirmPassword(!showConfirmPassword)} className="p-1"> - - - - - - - {loading ? ( - - ) : ( - - )} - {loading ? '处理中...' : '注册'} - - - ) - } + }, [phone, code]) return ( @@ -441,42 +120,81 @@ export default function Auth() { - - {(['login', 'register'] as const).map((tabMode) => { - const isActive = tabMode === mode || (mode === 'registerEmail' && tabMode === 'register') - return ( - { - if (tabMode === 'register') { - setMode('registerEmail') - } else { - setMode('login') - } - }} - > - - {tabMode === 'login' ? '登录' : '注册'} - - - ) - })} - - - {renderLogin()} - {renderRegister()} - {renderEmailSend()} + + 手机号 + + + + 0 ? undefined : handleSendCode} + className={`border-2 border-black px-[6px] py-[4px] ${canSendCode && countdown === 0 ? 'bg-black' : 'bg-gray-200'}`} + > + + {countdown > 0 ? `${countdown}秒` : '发送验证码'} + + + + + + + 验证码 + + + + + + + + {loading ? ( + + ) : ( + + )} + {loading ? '处理中...' : '登录'} + diff --git a/lib/auth.ts b/lib/auth.ts index e341a2c..292b059 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -114,7 +114,22 @@ export const authClient = createAuthClient({ ], }) -export const { signIn, signUp, signOut, useSession, $Infer, admin, forgetPassword, resetPassword, emailOtp } = - authClient +export const { + signIn, + signUp, + signOut, + useSession, + $Infer, + admin, + forgetPassword, + resetPassword, + emailOtp, + phoneNumber, +} = authClient + +// TODO: 手机号验证码发送接口还未实现 +// 需要后端提供手机号验证码发送接口,类似于 emailOtp.sendVerificationOtp +// 预期接口:phoneOtp.sendVerificationOtp({ phone, type: 'phone-verification' }) +// 当前在 app/auth.tsx 中的 handleSendCode 函数中使用了临时模拟代码,需要替换为真实 API 调用 export const subscription: ISubscription = Reflect.get(authClient, 'subscription') diff --git a/utils/index.ts b/utils/index.ts index bed5c57..054bf78 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -38,3 +38,9 @@ export const isValidEmail = (email: string) => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ return emailRegex.test(email) } + +// 手机号格式验证函数(支持中国大陆手机号) +export const isValidPhone = (phone: string) => { + const phoneRegex = /^1[3-9]\d{9}$/ + return phoneRegex.test(phone) +}