279 lines
12 KiB
TypeScript
279 lines
12 KiB
TypeScript
import { Ionicons } from '@expo/vector-icons'
|
||
import { Block, Text, Toast } from '@share/components'
|
||
import { router } from 'expo-router'
|
||
import React, { useCallback, useState } from 'react'
|
||
import { TextInput } from 'react-native'
|
||
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'
|
||
|
||
import BannerSection from '@/components/BannerSection'
|
||
import { emailOtp, forgetPassword } from '@/lib/auth'
|
||
import { isValidEmail } from '@/utils'
|
||
|
||
const APP_NAME = '多米'
|
||
|
||
export default function ForgotPassword() {
|
||
const [step, setStep] = useState<'send' | 'reset'>('send')
|
||
const [email, setEmail] = useState<string>('')
|
||
const [code, setCode] = useState<string>('') // 邮件中的验证码/token
|
||
const [newPassword, setNewPassword] = useState<string>('')
|
||
const [confirmPassword, setConfirmPassword] = useState<string>('')
|
||
const [loading, setLoading] = useState(false)
|
||
const [showNewPassword, setShowNewPassword] = useState<boolean>(false)
|
||
const [showConfirmPassword, setShowConfirmPassword] = useState<boolean>(false)
|
||
|
||
const handleSend = useCallback(async () => {
|
||
if (!email) {
|
||
Toast.show({ title: '请输入邮箱' })
|
||
return
|
||
}
|
||
if (!isValidEmail(email)) {
|
||
Toast.show({ title: '请输入正确的邮箱格式' })
|
||
return
|
||
}
|
||
setLoading(true)
|
||
try {
|
||
// better-auth的forgetPassword可能使用emailOtp方法
|
||
const result = await forgetPassword.emailOtp({ email })
|
||
if (result.error) {
|
||
Toast.show({ title: '邮箱未注册,发送失败' })
|
||
} else {
|
||
Toast.show({ title: '重置验证码已发送到邮箱,请查看邮箱后继续' })
|
||
setStep('reset')
|
||
}
|
||
} catch (err) {
|
||
const error = err as Error
|
||
Toast.show({ title: error?.message || '发送失败,请稍后重试' })
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [email])
|
||
|
||
const handleReset = useCallback(async () => {
|
||
if (!code) {
|
||
Toast.show({ title: '请输入邮件中的重置令牌' })
|
||
return
|
||
}
|
||
if (!newPassword || !confirmPassword) {
|
||
Toast.show({ title: '请输入新密码并确认' })
|
||
return
|
||
}
|
||
if (newPassword !== confirmPassword) {
|
||
Toast.show({ title: '两次密码输入不一致' })
|
||
return
|
||
}
|
||
if (newPassword.length < 6) {
|
||
Toast.show({ title: '密码长度至少6位' })
|
||
return
|
||
}
|
||
|
||
setLoading(true)
|
||
try {
|
||
const result = await emailOtp.resetPassword({ email, otp: code, password: newPassword })
|
||
|
||
if (result.error) {
|
||
Toast.show({ title: result.error.message || '重置失败,请稍后重试' })
|
||
} else {
|
||
Toast.show({ title: '密码已重置,请使用新密码登录' })
|
||
router.replace('/auth')
|
||
}
|
||
} catch (err) {
|
||
const error = err as Error
|
||
Toast.show({ title: error?.message || '重置失败,请稍后重试' })
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [code, newPassword, confirmPassword])
|
||
|
||
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={step === 'send' ? 'mail-outline' : 'key'} 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]">
|
||
{/* 发送邮箱步骤 */}
|
||
{step === 'send' && (
|
||
<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>
|
||
</Block>
|
||
)}
|
||
|
||
{/* 重置密码步骤 */}
|
||
{step === 'reset' && (
|
||
<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>
|
||
|
||
<Text className="font-900 my-[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" size={20} style={{ marginRight: 8 }} />
|
||
<TextInput
|
||
autoCapitalize="none"
|
||
placeholder="输入邮件中的重置令牌"
|
||
placeholderTextColor="#9CA3AF"
|
||
value={code}
|
||
style={{
|
||
flex: 1,
|
||
fontSize: 14,
|
||
fontWeight: 'bold',
|
||
color: 'black',
|
||
}}
|
||
onChangeText={setCode}
|
||
/>
|
||
</Block>
|
||
|
||
<Text className="font-900 my-[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" size={20} style={{ marginRight: 8 }} />
|
||
<TextInput
|
||
secureTextEntry={!showNewPassword}
|
||
placeholder="新密码"
|
||
placeholderTextColor="#9CA3AF"
|
||
value={newPassword}
|
||
style={{
|
||
flex: 1,
|
||
fontSize: 14,
|
||
fontWeight: 'bold',
|
||
color: 'black',
|
||
}}
|
||
onChangeText={setNewPassword}
|
||
/>
|
||
<Block onClick={() => setShowNewPassword(!showNewPassword)} className="p-1">
|
||
<Ionicons
|
||
color="black"
|
||
name={showNewPassword ? 'eye-off-outline' : 'eye-outline'}
|
||
size={20}
|
||
/>
|
||
</Block>
|
||
</Block>
|
||
|
||
<Text className="font-900 my-[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" 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={step === 'send' ? handleSend : handleReset}
|
||
>
|
||
{loading ? (
|
||
<Ionicons color="black" name="hourglass-outline" size={20} />
|
||
) : (
|
||
<Ionicons color="black" name={step === 'send' ? 'send' : 'key'} size={20} />
|
||
)}
|
||
<Text className="font-900 text-[16px] text-black">
|
||
{loading ? '处理中...' : step === 'send' ? '发送重置链接' : '重置密码'}
|
||
</Text>
|
||
</Block>
|
||
|
||
<Block className="mt-[4px] items-center">
|
||
<Text
|
||
className="font-700 text-[12px] text-gray-500"
|
||
onPress={() => {
|
||
if (step === 'send') router.replace('/auth')
|
||
else setStep('send')
|
||
}}
|
||
>
|
||
{step === 'send' ? '返回登录' : '返回发送重置邮件'}
|
||
</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>
|
||
</Block>
|
||
</Block>
|
||
</Block>
|
||
<Block className="h-[200px]" />
|
||
</KeyboardAwareScrollView>
|
||
</Block>
|
||
)
|
||
}
|