expo-duooomi-app/app/forgotPassword.tsx

279 lines
12 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, 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>
)
}