feat: 添加忘记密码功能,支持邮箱重置密码流程;优化登录逻辑,支持邮箱和用户名登录
This commit is contained in:
parent
3c9b5fcb08
commit
91131460af
|
|
@ -200,7 +200,7 @@ export default function Generate() {
|
|||
<BannerSection />
|
||||
|
||||
<FlashList
|
||||
contentContainerStyle={{ paddingHorizontal: 12 }}
|
||||
contentContainerStyle={{ paddingHorizontal: 12, paddingBottom: 200 }}
|
||||
data={displayTemplates}
|
||||
drawDistance={screenHeight}
|
||||
// @ts-ignore
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ function RootLayout() {
|
|||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="modal" options={{ headerShown: true }} />
|
||||
<Stack.Screen name="auth" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="forgot-password" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="pointList" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
</Providers>
|
||||
|
|
|
|||
117
app/auth.tsx
117
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<AuthMode>('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 (
|
||||
<Block className="relative flex-1 bg-black">
|
||||
<Block className="absolute inset-0 z-0 overflow-hidden">
|
||||
<VideoBox style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, opacity: 0.4 }} url={bgVideo} />
|
||||
<Block className="absolute inset-0 bg-black/20" />
|
||||
</Block>
|
||||
<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">
|
||||
|
|
@ -126,7 +150,7 @@ export default function Auth() {
|
|||
<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">LOOMART</Text>
|
||||
<Text className="font-900 mt-[16px] text-[32px] text-white">{APP_NAME}</Text>
|
||||
<Block className="mt-[8px] h-[4px] w-[120px] bg-accent" />
|
||||
</Block>
|
||||
|
||||
|
|
@ -167,7 +191,7 @@ export default function Auth() {
|
|||
<Ionicons color="black" name="person-outline" size={20} style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
placeholder="用户名"
|
||||
placeholder="用户名或邮箱"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={username}
|
||||
style={{
|
||||
|
|
@ -192,7 +216,7 @@ export default function Auth() {
|
|||
<TextInput
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
placeholder="your@email.com"
|
||||
placeholder=""
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={email}
|
||||
style={{
|
||||
|
|
@ -215,7 +239,7 @@ export default function Auth() {
|
|||
<Ionicons color="black" name="person-outline" size={20} style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
placeholder="username"
|
||||
placeholder=""
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={username}
|
||||
style={{
|
||||
|
|
@ -239,8 +263,8 @@ export default function Auth() {
|
|||
>
|
||||
<Ionicons color="black" name="lock-closed-outline" size={20} style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
secureTextEntry
|
||||
placeholder="••••••••"
|
||||
secureTextEntry={!showPassword}
|
||||
placeholder="请输入六位及以上密码"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={password}
|
||||
style={{
|
||||
|
|
@ -251,9 +275,23 @@ export default function Auth() {
|
|||
}}
|
||||
onChangeText={setPassword}
|
||||
/>
|
||||
<Block onClick={() => setShowPassword(!showPassword)} className="p-1">
|
||||
<Ionicons color="black" name={showPassword ? 'eye-off-outline' : 'eye-outline'} size={20} />
|
||||
</Block>
|
||||
</Block>
|
||||
</Block>
|
||||
|
||||
{mode === 'login' && (
|
||||
<Block className="items-end">
|
||||
<Text
|
||||
className="font-700 text-[12px] text-gray-500"
|
||||
onClick={() => router.push('/forgot-password')}
|
||||
>
|
||||
忘记密码
|
||||
</Text>
|
||||
</Block>
|
||||
)}
|
||||
|
||||
{mode === 'register' && (
|
||||
<Block>
|
||||
<Text className="font-900 mb-[8px] text-[12px] text-black">确认密码</Text>
|
||||
|
|
@ -263,8 +301,8 @@ export default function Auth() {
|
|||
>
|
||||
<Ionicons color="black" name="lock-closed-outline" size={20} style={{ marginRight: 8 }} />
|
||||
<TextInput
|
||||
secureTextEntry
|
||||
placeholder="••••••••"
|
||||
secureTextEntry={!showConfirmPassword}
|
||||
placeholder="请输入六位及以上密码"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={confirmPassword}
|
||||
style={{
|
||||
|
|
@ -275,6 +313,13 @@ export default function Auth() {
|
|||
}}
|
||||
onChangeText={setConfirmPassword}
|
||||
/>
|
||||
<Block onClick={() => setShowConfirmPassword(!showConfirmPassword)} className="p-1">
|
||||
<Ionicons
|
||||
color="black"
|
||||
name={showConfirmPassword ? 'eye-off-outline' : 'eye-outline'}
|
||||
size={20}
|
||||
/>
|
||||
</Block>
|
||||
</Block>
|
||||
</Block>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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<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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 = <TData, TParams = void>(
|
||||
apiFn: (params: TParams) => Promise<{ data?: TData; error?: ApiError }>,
|
||||
options?: {
|
||||
enabled?: boolean
|
||||
initialLoad?: boolean
|
||||
}
|
||||
},
|
||||
) => {
|
||||
const [data, setData] = useState<TData | undefined>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
|
@ -35,7 +36,7 @@ export const useLoomartApi = <TData, TParams = void>(
|
|||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[apiFn]
|
||||
[apiFn],
|
||||
)
|
||||
|
||||
return { data, loading, error, execute, refetch: execute }
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const PUBLIC_ROUTES = [
|
|||
'auth', // 登录注册页
|
||||
'pointList', // 积分列表页
|
||||
'explore',
|
||||
'forget-password',
|
||||
]
|
||||
|
||||
function isPublicRoute(segments: string[]): boolean {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
31
lib/auth.ts
31
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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue