expo-duooomi-app/app/auth.tsx

495 lines
17 KiB
TypeScript

import { Ionicons } from '@expo/vector-icons'
import { Block, Text, Toast } from '@share/components'
import { router } from 'expo-router'
import React, { useCallback, useMemo, 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 { emailOtp as emailOtpService, setAuthToken, signIn, signUp } from '@/lib/auth'
import { isValidEmail } from '@/utils'
type AuthMode = 'login' | 'register' | 'registerEmail'
const APP_NAME = '多米'
// 判断是否为邮箱格式的辅助函数
const isEmail = (input: string) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input)
}
export default function Auth() {
const [mode, setMode] = useState<AuthMode>('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 [loading, setLoading] = useState(false)
const canSend = useMemo(() => {
return isValidEmail(email)
}, [email])
const handleEmiallOtp = () => {
if (!canSend) {
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: '邮箱验证码验证失败' })
return
}
setMode('register')
} catch (error) {}
}
const handleLogin = useCallback(async () => {
if (!username || !password) {
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)
},
},
)
}
if (result.error) {
Toast.show({ title: '请输入正确的账号或密码' })
} else {
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 (
<Block className="gap-[16px]">
<Block>
<Text className="font-900 mb-[8px] text-[12px] text-black"></Text>
<Block
className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black"
style={{ height: 48 }}
>
<Ionicons color="black" name="person-outline" size={20} style={{ marginRight: 8 }} />
<TextInput
autoCapitalize="none"
placeholder="用户名/邮箱"
placeholderTextColor="#9CA3AF"
value={username}
style={{
flex: 1,
fontSize: 14,
fontWeight: 'bold',
color: 'black',
}}
onChangeText={setUsername}
/>
</Block>
</Block>
<Block>
<Text className="font-900 mb-[8px] text-[12px] text-black"></Text>
<Block
className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black"
style={{ height: 48 }}
>
<Ionicons color="black" name="lock-closed-outline" size={20} style={{ marginRight: 8 }} />
<TextInput
secureTextEntry={!showPassword}
placeholder="请输入密码"
placeholderTextColor="#9CA3AF"
value={password}
style={{
flex: 1,
fontSize: 14,
fontWeight: 'bold',
color: 'black',
}}
onChangeText={setPassword}
/>
<Block onClick={() => setShowPassword(!showPassword)} className="p-1">
<Ionicons color="black" name={showPassword ? 'eye-off-outline' : 'eye-outline'} size={20} />
</Block>
</Block>
</Block>
<Block className="items-end">
<Text className="font-700 text-[12px] text-gray-500" onClick={() => router.push('/forgotPassword')}>
</Text>
</Block>
<Block
className={`font-900 mt-[8px] flex-row items-center justify-center gap-[8px] border-[3px] border-black py-[14px] shadow-hard-black ${loading ? 'bg-gray-300' : 'bg-accent'}`}
onClick={handleLogin}
>
{loading ? (
<Ionicons color="black" name="hourglass-outline" size={20} />
) : (
<Ionicons color="black" name="flash" size={20} />
)}
<Text className="font-900 text-[16px] text-black">{loading ? '处理中...' : '登录'}</Text>
</Block>
</Block>
)
}
const renderEmailSend = () => {
if (mode !== 'registerEmail') {
return null
}
return (
<Block className="gap-[16px]">
<Block>
<Text className="font-900 mb-[8px] text-[12px] text-black"></Text>
<Block
className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black"
style={{ height: 48 }}
>
<Ionicons color="black" name="mail-outline" size={20} style={{ marginRight: 8 }} />
<TextInput
autoCapitalize="none"
keyboardType="email-address"
placeholder="请输入邮箱"
placeholderTextColor="#9CA3AF"
value={email}
style={{
flex: 1,
fontSize: 14,
fontWeight: 'bold',
color: 'black',
}}
onChangeText={setEmail}
/>
<Block
onClick={handleEmiallOtp}
className={`border-2 border-black px-[6px] py-[4px] ${canSend ? 'bg-black' : 'bg-gray-200'}`}
>
<Text className={`text-[10px] font-[900] ${canSend ? 'text-accent' : 'text-gray-500'}`}></Text>
</Block>
</Block>
</Block>
<Block>
<Text className="font-900 mb-[8px] text-[12px] text-black"></Text>
<Block
className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black"
style={{ height: 48 }}
>
<Ionicons color="black" name="mail-outline" size={20} style={{ marginRight: 8 }} />
<TextInput
autoCapitalize="none"
placeholder="输入邮件中的验证码"
placeholderTextColor="#9CA3AF"
value={emailOtp}
style={{
flex: 1,
fontSize: 14,
fontWeight: 'bold',
color: 'black',
}}
onChangeText={setEmailOtp}
/>
</Block>
</Block>
<Block
className={`font-900 mt-[8px] flex-row items-center justify-center gap-[8px] border-[3px] border-black py-[14px] shadow-hard-black ${loading ? 'bg-gray-300' : 'bg-accent'}`}
onClick={handleEmailVerify}
>
{loading ? (
<Ionicons color="black" name="hourglass-outline" size={20} />
) : (
<Ionicons color="black" name="flash" size={20} />
)}
<Text className="font-900 text-[16px] text-black"></Text>
</Block>
</Block>
)
}
const renderRegister = () => {
if (mode !== 'register') {
return null
}
return (
<Block className="gap-[16px]">
<Block>
<Text className="font-900 mb-[8px] text-[12px] text-black"></Text>
<Block
className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black"
style={{ height: 48 }}
>
<Ionicons color="black" name="person-outline" size={20} style={{ marginRight: 8 }} />
<TextInput
autoCapitalize="none"
placeholder="用户名"
placeholderTextColor="#9CA3AF"
value={username}
style={{
flex: 1,
fontSize: 14,
fontWeight: 'bold',
color: 'black',
}}
onChangeText={setUsername}
/>
</Block>
</Block>
<Block>
<Text className="font-900 mb-[8px] text-[12px] text-black"></Text>
<Block
className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black"
style={{ height: 48 }}
>
<Ionicons color="black" name="lock-closed-outline" size={20} style={{ marginRight: 8 }} />
<TextInput
secureTextEntry={!showPassword}
placeholder="请输入密码"
placeholderTextColor="#9CA3AF"
value={password}
style={{
flex: 1,
fontSize: 14,
fontWeight: 'bold',
color: 'black',
}}
onChangeText={setPassword}
/>
<Block onClick={() => setShowPassword(!showPassword)} className="p-1">
<Ionicons color="black" name={showPassword ? 'eye-off-outline' : 'eye-outline'} size={20} />
</Block>
</Block>
</Block>
<Block>
<Text className="font-900 mb-[8px] text-[12px] text-black"></Text>
<Block
className="flex-row items-center border-[3px] border-black bg-white px-[12px] shadow-medium-black"
style={{ height: 48 }}
>
<Ionicons color="black" name="lock-closed-outline" size={20} style={{ marginRight: 8 }} />
<TextInput
secureTextEntry={!showConfirmPassword}
placeholder="请再次输入密码"
placeholderTextColor="#9CA3AF"
value={confirmPassword}
style={{
flex: 1,
fontSize: 14,
fontWeight: 'bold',
color: 'black',
}}
onChangeText={setConfirmPassword}
/>
<Block onClick={() => setShowConfirmPassword(!showConfirmPassword)} className="p-1">
<Ionicons color="black" name={showConfirmPassword ? 'eye-off-outline' : 'eye-outline'} size={20} />
</Block>
</Block>
</Block>
<Block
className={`font-900 mt-[8px] flex-row items-center justify-center gap-[8px] border-[3px] border-black py-[14px] shadow-hard-black ${loading ? 'bg-gray-300' : 'bg-accent'}`}
onClick={handleRegister}
>
{loading ? (
<Ionicons color="black" name="hourglass-outline" size={20} />
) : (
<Ionicons color="black" name="flash" size={20} />
)}
<Text className="font-900 text-[16px] text-black">{loading ? '处理中...' : '注册'}</Text>
</Block>
</Block>
)
}
return (
<Block className="relative flex-1 bg-black">
<BannerSection />
<KeyboardAwareScrollView bottomOffset={100}>
<Block className="flex-1 items-center justify-center px-[24px] py-[40px]">
<Block className="relative w-full max-w-screen-xs">
<Block className="relative mb-[32px] items-center">
<Block className="size-[80px] items-center justify-center rounded-full border-4 border-black bg-accent shadow-deep-black">
<Ionicons color="black" name="flash" size={40} />
</Block>
<Text className="font-900 mt-[16px] text-[32px] text-white">{APP_NAME}</Text>
<Block className="mt-[8px] h-[4px] w-[120px] bg-accent" />
</Block>
<Block className="mb-[24px] flex-row gap-[12px]">
{(['login', 'register'] as const).map((tabMode) => {
const isActive = tabMode === mode || (mode === 'registerEmail' && tabMode === 'register')
return (
<Block
key={tabMode}
className={`flex-1 items-center justify-center border-[3px] border-black py-[12px] shadow-hard-black ${isActive ? 'bg-accent' : 'bg-white'}`}
style={{ transform: [{ skewX: '-6deg' }] }}
onClick={() => {
if (tabMode === 'register') {
setMode('registerEmail')
} else {
setMode('login')
}
}}
>
<Text
className={`font-900 text-[14px] ${isActive ? 'text-black' : 'text-gray-500'}`}
style={{ transform: [{ skewX: '6deg' }] }}
>
{tabMode === 'login' ? '登录' : '注册'}
</Text>
</Block>
)
})}
</Block>
<Block
className="relative border-4 border-black bg-white p-[24px] shadow-deep-black"
style={{ transform: [{ skewX: '-3deg' }] }}
>
<Block style={{ transform: [{ skewX: '3deg' }] }}>
<Block className="mt-[4px] gap-[16px]">
{renderLogin()}
{renderRegister()}
{renderEmailSend()}
</Block>
</Block>
</Block>
<Block className="mt-[24px] items-center">
<Text className="font-700 text-[12px] text-gray-400">© 2025 LOOMART. All rights reserved.</Text>
<Text className="font-700 text-[12px] text-gray-400">: {APP_VERSION}</Text>
</Block>
</Block>
</Block>
<Block className="h-[200px]" />
</KeyboardAwareScrollView>
</Block>
)
}