diff --git a/app/(tabs)/generate.tsx b/app/(tabs)/generate.tsx
index 8a615e6..b134c78 100644
--- a/app/(tabs)/generate.tsx
+++ b/app/(tabs)/generate.tsx
@@ -200,7 +200,7 @@ export default function Generate() {
+
diff --git a/app/auth.tsx b/app/auth.tsx
index 2775927..aad5edd 100644
--- a/app/auth.tsx
+++ b/app/auth.tsx
@@ -1,30 +1,33 @@
import { Ionicons } from '@expo/vector-icons'
-import { Block, Text, Toast, VideoBox } from '@share/components'
+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 { useAuth } from '@/hooks/core/use-auth'
import { setAuthToken } from '@/lib/auth'
-const BACKGROUND_VIDEOS = [
- 'https://cdn.roasmax.cn/material/b46f380532e14cf58dd350dbacc7c34a.mp4',
- 'https://cdn.roasmax.cn/material/992e6c5d940c42feb71c27e556b754c0.mp4',
- 'https://cdn.roasmax.cn/material/e4947477843f4067be7c37569a33d17b.mp4',
-]
-
type AuthMode = 'login' | 'register'
+const APP_NAME = '多米'
+
+// 判断是否为邮箱格式的辅助函数
+const isEmail = (input: string) => {
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input)
+}
+
export default function Auth() {
const { signIn, signUp } = useAuth()
const [mode, setMode] = useState('login')
- const [bgVideo] = useState(() => BACKGROUND_VIDEOS[Math.floor(Math.random() * BACKGROUND_VIDEOS.length)])
const [username, setUsername] = useState('')
const [email, setEmail] = 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 handleLogin = useCallback(async () => {
@@ -35,26 +38,50 @@ export default function Auth() {
setLoading(true)
try {
- const result = await signIn.username(
- {
- username,
- password,
- },
- {
- onSuccess: async (ctx) => {
- const authToken = ctx.response.headers.get('set-auth-token')
- if (authToken) {
- setAuthToken(authToken)
- }
+ let result
+
+ if (isEmail(username)) {
+ // 如果用户名是邮箱格式,使用邮箱登录
+ result = await signIn.email(
+ {
+ email: username,
+ password,
},
- onError: (ctx) => {
- console.error(`[LOGIN] login error`, ctx)
+ {
+ 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: result.error.message || '登录失败' })
+ Toast.show({ title: '请输入正确的账号或密码' })
} else {
Toast.show({ title: '登录成功!' })
router.replace('/(tabs)')
@@ -115,10 +142,7 @@ export default function Auth() {
return (
-
-
-
-
+
@@ -126,7 +150,7 @@ export default function Auth() {
- LOOMART
+ {APP_NAME}
@@ -167,7 +191,7 @@ export default function Auth() {
+ setShowPassword(!showPassword)} className="p-1">
+
+
+ {mode === 'login' && (
+
+ router.push('/forgot-password')}
+ >
+ 忘记密码
+
+
+ )}
+
{mode === 'register' && (
确认密码
@@ -263,8 +301,8 @@ export default function Auth() {
>
+ setShowConfirmPassword(!showConfirmPassword)} className="p-1">
+
+
)}
diff --git a/app/forgot-password.tsx b/app/forgot-password.tsx
new file mode 100644
index 0000000..cc71750
--- /dev/null
+++ b/app/forgot-password.tsx
@@ -0,0 +1,284 @@
+// ...existing code...
+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'
+
+const APP_NAME = '多米'
+
+// 邮箱格式验证函数
+const isValidEmail = (email: string) => {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+ return emailRegex.test(email)
+}
+
+export default function ForgotPassword() {
+ const [step, setStep] = useState<'send' | 'reset'>('send')
+ const [email, setEmail] = useState('')
+ const [code, setCode] = useState('') // 邮件中的验证码/token
+ const [newPassword, setNewPassword] = useState('')
+ const [confirmPassword, setConfirmPassword] = useState('')
+ const [loading, setLoading] = useState(false)
+ const [showNewPassword, setShowNewPassword] = useState(false)
+ const [showConfirmPassword, setShowConfirmPassword] = useState(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 (
+
+
+
+
+
+
+
+
+
+ {APP_NAME}
+
+
+
+
+
+
+ {/* 发送邮箱步骤 */}
+ {step === 'send' && (
+
+ 邮箱
+
+
+
+
+
+ )}
+
+ {/* 重置密码步骤 */}
+ {step === 'reset' && (
+
+ 邮箱
+
+
+
+
+
+ 重置令牌(邮件中获取)
+
+
+
+
+
+ 新密码
+
+
+
+ setShowNewPassword(!showNewPassword)} className="p-1">
+
+
+
+
+ 确认密码
+
+
+
+ setShowConfirmPassword(!showConfirmPassword)} className="p-1">
+
+
+
+
+ )}
+
+
+ {loading ? (
+
+ ) : (
+
+ )}
+
+ {loading ? '处理中...' : step === 'send' ? '发送重置链接' : '重置密码'}
+
+
+
+
+ {
+ if (step === 'send') router.replace('/auth')
+ else setStep('send')
+ }}
+ >
+ {step === 'send' ? '返回登录' : '返回发送重置邮件'}
+
+
+
+
+
+
+
+ © 2025 LOOMART. All rights reserved.
+
+
+
+
+
+
+ )
+}
diff --git a/hooks/core/use-loomart-api.ts b/hooks/core/use-loomart-api.ts
index 69bce8f..46f1158 100644
--- a/hooks/core/use-loomart-api.ts
+++ b/hooks/core/use-loomart-api.ts
@@ -1,12 +1,13 @@
-import { ApiError } from "@/lib/types"
-import { useState, useCallback } from "react"
+import { useCallback, useState } from 'react'
+
+import { type ApiError } from '@/lib/types'
export const useLoomartApi = (
apiFn: (params: TParams) => Promise<{ data?: TData; error?: ApiError }>,
options?: {
enabled?: boolean
initialLoad?: boolean
- }
+ },
) => {
const [data, setData] = useState()
const [loading, setLoading] = useState(false)
@@ -35,7 +36,7 @@ export const useLoomartApi = (
setLoading(false)
}
},
- [apiFn]
+ [apiFn],
)
return { data, loading, error, execute, refetch: execute }
diff --git a/hooks/core/use-route-guard.ts b/hooks/core/use-route-guard.ts
index 3598830..f2b9918 100644
--- a/hooks/core/use-route-guard.ts
+++ b/hooks/core/use-route-guard.ts
@@ -10,6 +10,7 @@ const PUBLIC_ROUTES = [
'auth', // 登录注册页
'pointList', // 积分列表页
'explore',
+ 'forget-password',
]
function isPublicRoute(segments: string[]): boolean {
diff --git a/hooks/core/use-user-balance.ts b/hooks/core/use-user-balance.ts
index 0b0fa98..c8213dd 100644
--- a/hooks/core/use-user-balance.ts
+++ b/hooks/core/use-user-balance.ts
@@ -1,6 +1,7 @@
-import { subscription } from "@/lib/auth"
-import { ApiError } from "@/lib/types"
-import { useState } from "react"
+import { useState } from 'react'
+
+import { subscription } from '@/lib/auth'
+import { type ApiError } from '@/lib/types'
export const useUserBalance = () => {
const [loading, setLoading] = useState(false)
@@ -17,7 +18,7 @@ export const useUserBalance = () => {
return
}
- const meteredSubscriptions = data?.filter(sub => sub.type === 'metered') || []
+ const meteredSubscriptions = data?.filter((sub) => sub.type === 'metered') || []
const creditBalance = meteredSubscriptions[0]?.creditBalance?.remainingTokenBalance || 0
setBalance(creditBalance)
} catch (e) {
diff --git a/lib/auth.ts b/lib/auth.ts
index fee1566..91e79c5 100644
--- a/lib/auth.ts
+++ b/lib/auth.ts
@@ -1,21 +1,23 @@
import 'reflect-metadata'
-import { createAuthClient } from 'better-auth/react'
-import {
- usernameClient,
- phoneNumberClient,
- emailOTPClient,
- genericOAuthClient,
- adminClient,
- organizationClient,
- jwtClient,
-} from 'better-auth/client/plugins'
+
import { expoClient } from '@better-auth/expo/client'
import { createSkerClientPlugin } from '@repo/sdk'
-import { stripeClient } from './plugins/stripe-plugin'
-import type { Subscription } from './plugins/stripe'
-import type { ApiError } from './types'
+import {
+ adminClient,
+ emailOTPClient,
+ genericOAuthClient,
+ jwtClient,
+ organizationClient,
+ phoneNumberClient,
+ usernameClient,
+} from 'better-auth/client/plugins'
+import { createAuthClient } from 'better-auth/react'
+
import { fetchWithLogger } from './fetch-logger'
+import type { Subscription } from './plugins/stripe'
+import { stripeClient } from './plugins/stripe-plugin'
import { storage } from './storage'
+import type { ApiError } from './types'
export interface CreditBalance {
tokenUsage: number
@@ -106,6 +108,7 @@ export const authClient = createAuthClient({
],
})
-export const { signIn, signUp, signOut, useSession, $Infer, admin } = authClient
+export const { signIn, signUp, signOut, useSession, $Infer, admin, forgetPassword, resetPassword, emailOtp } =
+ authClient
export const subscription: ISubscription = Reflect.get(authClient, 'subscription')
diff --git a/lib/fetch-logger.ts b/lib/fetch-logger.ts
index 948ced8..305945e 100644
--- a/lib/fetch-logger.ts
+++ b/lib/fetch-logger.ts
@@ -1,5 +1,3 @@
-import { router } from 'expo-router'
-
import { storage } from './storage'
interface FetchLoggerOptions {
@@ -43,7 +41,7 @@ export const createFetchWithLogger = (options: FetchLoggerOptions = {}) => {
if (response.status === 401) {
console.warn('🔐 401 未授权,跳转到登录页')
- router.replace('/auth')
+ // router.replace('/auth')
}
return response