expo-duooomi-app/app/auth.tsx

245 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { phoneNumber, setAuthToken } from '@/lib/auth'
import { isValidPhone } from '@/utils'
import { openUrl } from '@/utils/webview-helper'
const APP_NAME = '多米'
export default function Auth() {
const [phone, setPhone] = useState('')
const [code, setCode] = useState('')
const [loading, setLoading] = useState(false)
const [countdown, setCountdown] = useState(0)
const [agreed, setAgreed] = useState(false)
const canSendCode = useMemo(() => {
return isValidPhone(phone)
}, [phone])
// TODO: 获取验证码接口还未实现,需要后端提供手机号验证码发送接口
const handleSendCode = useCallback(async () => {
if (!canSendCode) {
Toast.show({ title: '请输入有效的手机号' })
return
}
Toast.showLoading({ title: '正在发送验证码...' })
setLoading(true)
try {
// TODO: 调用获取验证码接口
// 当前使用 better-auth 的 phoneNumber.sendOtp但后端可能还未实现
// 如果后端未实现,这里会报错,需要后端提供手机号验证码发送接口
const result = await phoneNumber.sendOtp({
phoneNumber: phone,
})
if (result.error) {
Toast.show({ title: result.error.message || '验证码发送失败,请检查后端接口是否已实现' })
return
}
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 (!phone || !code) {
Toast.show({ title: '请填写手机号和验证码' })
return
}
if (!isValidPhone(phone)) {
Toast.show({ title: '请输入有效的手机号' })
return
}
if (!agreed) {
Toast.show({ title: '请先阅读并同意服务条款和隐私协议' })
return
}
setLoading(true)
try {
// 使用 better-auth 的 phoneNumber.verify 进行验证和登录
// verify 方法会自动创建会话,如果用户不存在会自动注册
const result = await phoneNumber.verify({
phoneNumber: phone,
code,
updatePhoneNumber: true, // 如果已登录,更新手机号
})
if (result.error) {
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)
}
}, [phone, code, agreed])
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="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]">
<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="phone-portrait-outline" size={20} style={{ marginRight: 8 }} />
<TextInput
autoCapitalize="none"
keyboardType="phone-pad"
placeholder="请输入手机号"
placeholderTextColor="#9CA3AF"
value={phone}
maxLength={11}
style={{
flex: 1,
fontSize: 14,
fontWeight: 'bold',
color: 'black',
}}
onChangeText={setPhone}
/>
<Block
onClick={countdown > 0 ? undefined : handleSendCode}
className={`border-2 border-black px-[6px] py-[4px] ${canSendCode && countdown === 0 ? 'bg-black' : 'bg-gray-200'}`}
>
<Text className={`text-[10px] font-[900] ${canSendCode && countdown === 0 ? 'text-accent' : 'text-gray-500'}`}>
{countdown > 0 ? `${countdown}` : '发送验证码'}
</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="key-outline" size={20} style={{ marginRight: 8 }} />
<TextInput
autoCapitalize="none"
keyboardType="number-pad"
placeholder="请输入验证码"
placeholderTextColor="#9CA3AF"
value={code}
maxLength={6}
style={{
flex: 1,
fontSize: 14,
fontWeight: 'bold',
color: 'black',
}}
onChangeText={setCode}
/>
</Block>
</Block>
<Block className="mt-[8px] flex-row items-center gap-[8px]">
<Block
onClick={() => setAgreed(!agreed)}
className={`size-[20px] items-center justify-center border-[2px] border-black ${agreed ? 'bg-black' : 'bg-white'}`}
>
{agreed && <Ionicons color="#FFE500" name="checkmark" size={14} />}
</Block>
<Block className="flex-row flex-wrap items-center">
<Text className="font-700 text-[12px] text-black"></Text>
<Text
className="font-700 text-[12px] text-black underline"
onClick={() => openUrl('https://mixvideo.bowong.cc/terms', '服务条款')}
>
</Text>
<Text className="font-700 text-[12px] text-black"></Text>
<Text
className="font-700 text-[12px] text-black underline"
onClick={() => openUrl('https://mixvideo.bowong.cc/privacy', '隐私协议')}
>
</Text>
</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={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>
</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>
)
}