diff --git a/app/_layout.tsx b/app/_layout.tsx
index 0f90027..e670a54 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -1,43 +1,110 @@
import '../global.css'
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'
-import { Stack } from 'expo-router'
+import { Stack, usePathname, useRouter } from 'expo-router'
import { StatusBar } from 'expo-status-bar'
+import { useEffect } from 'react'
import 'react-native-reanimated'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
+import { View, ActivityIndicator } from 'react-native'
import { useColorScheme } from '@/hooks/use-color-scheme'
import { KeyboardProvider } from 'react-native-keyboard-controller'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { ModalPortal } from '@/components/ui'
import '@/lib/i18n'
+import { useSession } from '@/lib/auth'
export const unstable_settings = {
anchor: '(tabs)',
}
+// 受保护的路由(需要登录才能访问)
+const PROTECTED_ROUTES = [
+ '/(tabs)',
+ '/channels',
+ '/generateVideo',
+ '/generationRecord',
+ '/worksList',
+ '/membership',
+ '/changePassword',
+]
+
+// 公开路由(无需登录)
+const PUBLIC_ROUTES = [
+ '/auth',
+ '/modal',
+ '/searchTemplate',
+ '/searchResults',
+ '/searchWorks',
+ '/searchWorksResults',
+ '/templateDetail',
+ '/terms',
+ '/privacy',
+]
+
+// 认证守卫组件
+function AuthGuard({ children }: { children: React.ReactNode }) {
+ const pathname = usePathname()
+ const router = useRouter()
+ const { data: session, isPending } = useSession()
+
+ useEffect(() => {
+ // 加载中不处理
+ if (isPending) return
+
+ // 检查是否在受保护路由
+ const isProtectedRoute = PROTECTED_ROUTES.some(route => pathname?.startsWith(route))
+
+ // 未登录访问受保护路由 -> 跳转到登录页
+ if (isProtectedRoute && !session?.user) {
+ router.replace('/auth')
+ return
+ }
+
+ // 已登录访问登录页 -> 跳转到首页
+ if (pathname === '/auth' && session?.user) {
+ router.replace('/(tabs)')
+ return
+ }
+ }, [pathname, session, isPending, router])
+
+ // 加载中显示 loading
+ if (isPending) {
+ return (
+
+
+
+ )
+ }
+
+ return <>{children}>
+}
+
// 路由层
export default function RootLayout() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
diff --git a/lib/auth.ts b/lib/auth.ts
index 7febea8..a9a76fa 100644
--- a/lib/auth.ts
+++ b/lib/auth.ts
@@ -1,5 +1,6 @@
import 'reflect-metadata'
+import Constants from 'expo-constants'
import { expoClient } from '@better-auth/expo/client'
import { createSkerClientPlugin } from '@repo/sdk'
import {
@@ -13,12 +14,15 @@ import {
} from 'better-auth/client/plugins'
import { createAuthClient } from 'better-auth/react'
-import { fetchWithLogger } from './fetch-logger'
+import { fetchWithLogger, TOKEN_KEY } from './fetch-logger'
import type { Subscription } from './plugins/stripe'
import { stripeClient } from './plugins/stripe-plugin'
import { storage } from './storage'
import type { ApiError } from './types'
+// 商户ID配置 - 从环境变量或应用配置中读取
+const MERCHANT_ID = Constants.expoConfig?.extra?.merchantId || ''
+
export interface CreditBalance {
tokenUsage: number
remainingTokenBalance: number
@@ -72,10 +76,11 @@ export interface ISubscription {
restore: (params?: SubscriptionRestoreParams) => Promise<{ data?: SubscriptionRestoreResponse; error?: ApiError }>
billingPortal: (params?: BillingPortalParams) => Promise<{ data?: BillingPortalResponse; error?: ApiError }>
}
-export const getAuthToken = async () => (await storage.getItem('bestaibest.better-auth.session_token')) || ''
+// 统一的 Token 存储键名(与 fetch-logger.ts 保持一致)
+export const getAuthToken = async () => (await storage.getItem(TOKEN_KEY)) || ''
export const setAuthToken = async (token: string) => {
- await storage.setItem(`bestaibest.better-auth.session_token`, token)
+ await storage.setItem(TOKEN_KEY, token)
}
export const authClient = createAuthClient({
baseURL: 'https://api.mixvideo.bowong.cc',
@@ -84,7 +89,9 @@ export const authClient = createAuthClient({
scheme: 'duooomi',
fetchOptions: {
headers: {
- "x-ownerid": ""
+ // x-ownerid: 商户ID(如果后端需要)
+ // 如果 MERCHANT_ID 为空,则不添加此 header
+ ...(MERCHANT_ID && { 'x-ownerid': MERCHANT_ID }),
},
auth: {
type: 'Bearer',
@@ -114,4 +121,8 @@ export const authClient = createAuthClient({
export const { signIn, signUp, signOut, useSession, $Infer, admin, forgetPassword, resetPassword, emailOtp } =
authClient
+// 导出 loomart API(来自 createSkerClientPlugin)
+export const loomart = Reflect.get(authClient, 'loomart')
+
+// 导出 subscription API
export const subscription: ISubscription = Reflect.get(authClient, 'subscription')
diff --git a/lib/fetch-logger.ts b/lib/fetch-logger.ts
index ac68839..0fc004e 100644
--- a/lib/fetch-logger.ts
+++ b/lib/fetch-logger.ts
@@ -1,6 +1,10 @@
-import { router } from 'expo-router'
+import { router } from 'expo-router';
+import { Toast } from '@/components/ui/Toast';
-import { storage } from './storage'
+import { storage } from './storage';
+
+// 统一的 Token 存储键名(与 lib/auth.ts 保持一致)
+export const TOKEN_KEY = 'bestaibest.better-auth.session_token';
interface FetchLoggerOptions {
enableLogging?: boolean
@@ -18,6 +22,9 @@ const defaultOptions: FetchLoggerOptions = {
const originalFetch = global.fetch
+// 防止重复跳转的标志
+let isRedirecting = false
+
export const createFetchWithLogger = (options: FetchLoggerOptions = {}) => {
const config = { ...defaultOptions, ...options }
@@ -27,7 +34,8 @@ export const createFetchWithLogger = (options: FetchLoggerOptions = {}) => {
const method = init?.method || 'GET'
try {
- const token = await storage.getItem('token')
+ // 使用统一的 Token 存储键
+ const token = await storage.getItem(TOKEN_KEY)
if (token) {
init = {
@@ -41,10 +49,24 @@ export const createFetchWithLogger = (options: FetchLoggerOptions = {}) => {
const response = await originalFetch(input, init)
- if (response.status === 401) {
+ // 401 未授权处理
+ if (response.status === 401 && !isRedirecting) {
console.warn('🔐 401 未授权,跳转到登录页')
- // router.replace('/auth')
- router.push('/auth')
+ isRedirecting = true
+
+ // 显示 Toast 提示
+ Toast.show('登录已过期,请重新登录')
+
+ // 清除过期 Token
+ await storage.removeItem(TOKEN_KEY)
+
+ // 跳转到登录页(使用 replace 避免返回栈问题)
+ router.replace('/auth')
+
+ // 延迟重置标志(防止并发请求导致重复跳转)
+ setTimeout(() => {
+ isRedirecting = false
+ }, 1000)
}
return response
diff --git a/lib/storage.native.ts b/lib/storage.native.ts
index 8b13789..555fdea 100644
--- a/lib/storage.native.ts
+++ b/lib/storage.native.ts
@@ -1 +1,28 @@
+import AsyncStorage from '@react-native-async-storage/async-storage';
+export const storage = {
+ async getItem(key: string): Promise {
+ try {
+ return await AsyncStorage.getItem(key);
+ } catch (error) {
+ console.error(`[Storage] Error getting item "${key}":`, error);
+ return null;
+ }
+ },
+
+ async setItem(key: string, value: string): Promise {
+ try {
+ await AsyncStorage.setItem(key, value);
+ } catch (error) {
+ console.error(`[Storage] Error setting item "${key}":`, error);
+ }
+ },
+
+ async removeItem(key: string): Promise {
+ try {
+ await AsyncStorage.removeItem(key);
+ } catch (error) {
+ console.error(`[Storage] Error removing item "${key}":`, error);
+ }
+ },
+};
diff --git a/lib/storage.ts b/lib/storage.ts
index 73c788e..5283924 100644
--- a/lib/storage.ts
+++ b/lib/storage.ts
@@ -1,17 +1,36 @@
+/**
+ * Web平台存储实现 (使用 localStorage)
+ * Native平台会自动使用 storage.native.ts (使用 AsyncStorage)
+ */
+
declare const window: any;
+
export const storage = {
async getItem(key: string): Promise {
- if (typeof window === "undefined") return null;
- return window.localStorage.getItem(key);
+ try {
+ if (typeof window === "undefined") return null;
+ return window.localStorage.getItem(key);
+ } catch (error) {
+ console.error(`[Storage] Error getting item "${key}":`, error);
+ return null;
+ }
},
async setItem(key: string, value: string): Promise {
- if (typeof window === "undefined") return;
- window.localStorage.setItem(key, value);
+ try {
+ if (typeof window === "undefined") return;
+ window.localStorage.setItem(key, value);
+ } catch (error) {
+ console.error(`[Storage] Error setting item "${key}":`, error);
+ }
},
async removeItem(key: string): Promise {
- if (typeof window === "undefined") return;
- window.localStorage.removeItem(key);
+ try {
+ if (typeof window === "undefined") return;
+ window.localStorage.removeItem(key);
+ } catch (error) {
+ console.error(`[Storage] Error removing item "${key}":`, error);
+ }
},
};