diff --git a/src/app/(loading)/Redirect.tsx b/src/app/(loading)/Redirect.tsx index 14fe6f86e2a9..bc7551fc9501 100644 --- a/src/app/(loading)/Redirect.tsx +++ b/src/app/(loading)/Redirect.tsx @@ -3,35 +3,47 @@ import { useRouter } from 'next/navigation'; import { memo, useEffect } from 'react'; -import { messageService } from '@/services/message'; -import { sessionService } from '@/services/session'; import { useUserStore } from '@/store/user'; import { authSelectors } from '@/store/user/selectors'; -const checkHasConversation = async () => { - const hasMessages = await messageService.hasMessages(); - const hasAgents = await sessionService.hasSessions(); - return hasMessages || hasAgents; -}; - const Redirect = memo(() => { const router = useRouter(); - const isLogin = useUserStore(authSelectors.isLogin); + const [isLogin, isLoaded, isUserStateInit, isUserHasConversation, isOnboard] = useUserStore( + (s) => [ + authSelectors.isLogin(s), + authSelectors.isLoaded(s), + s.isUserStateInit, + s.isUserHasConversation, + s.isOnboard, + ], + ); useEffect(() => { + // if user auth state is not ready, wait for loading + if (!isLoaded) return; + + // this mean user is definitely not login if (!isLogin) { router.replace('/welcome'); return; } - checkHasConversation().then((hasData) => { - if (hasData) { - router.replace('/chat'); - } else { - router.replace('/welcome'); - } - }); - }, []); + // if user state not init, wait for loading + if (!isUserStateInit) return; + + // user need to onboard + if (!isOnboard) { + router.replace('/onboard'); + return; + } + + // finally check the conversation status + if (isUserHasConversation) { + router.replace('/chat'); + } else { + router.replace('/welcome'); + } + }, [isUserStateInit, isLoaded, isUserHasConversation, isOnboard, isLogin]); return null; }); diff --git a/src/layout/AuthProvider/NoAuth/index.tsx b/src/layout/AuthProvider/NoAuth/index.tsx new file mode 100644 index 000000000000..2b6d449f2574 --- /dev/null +++ b/src/layout/AuthProvider/NoAuth/index.tsx @@ -0,0 +1,16 @@ +'use client'; + +import { PropsWithChildren, memo } from 'react'; +import { createStoreUpdater } from 'zustand-utils'; + +import { useUserStore } from '@/store/user'; + +const NoAuthProvider = memo(({ children }) => { + const useStoreUpdater = createStoreUpdater(useUserStore); + + useStoreUpdater('isLoaded', true); + + return children; +}); + +export default NoAuthProvider; diff --git a/src/layout/AuthProvider/index.tsx b/src/layout/AuthProvider/index.tsx index 0d8435b219ac..e27f031c3831 100644 --- a/src/layout/AuthProvider/index.tsx +++ b/src/layout/AuthProvider/index.tsx @@ -4,13 +4,14 @@ import { authEnv } from '@/config/auth'; import Clerk from './Clerk'; import NextAuth from './NextAuth'; +import NoAuth from './NoAuth'; const AuthProvider = ({ children }: PropsWithChildren) => { if (authEnv.NEXT_PUBLIC_ENABLE_CLERK_AUTH) return {children}; if (authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH) return {children}; - return children; + return {children}; }; export default AuthProvider; diff --git a/src/middleware.ts b/src/middleware.ts index 6ab41016c57e..a3036885fb1e 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -41,16 +41,7 @@ const nextAuthMiddleware = auth((req) => { }); export default authEnv.NEXT_PUBLIC_ENABLE_CLERK_AUTH - ? // can't lift to a function because if there is no clerk public key, it will throw error - clerkMiddleware((auth, request) => { - // if user is logged in and on the home page, redirect to chat - if (auth().userId && request.nextUrl.pathname === '/') { - request.nextUrl.pathname = '/chat'; - return NextResponse.redirect(request.nextUrl); - } - - return NextResponse.next(); - }) + ? clerkMiddleware() : authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH ? nextAuthMiddleware : defaultMiddleware; diff --git a/src/services/user/client.test.ts b/src/services/user/client.test.ts index bbaff3cdcada..eb455cbdbfd9 100644 --- a/src/services/user/client.test.ts +++ b/src/services/user/client.test.ts @@ -46,6 +46,8 @@ describe('ClientService', () => { expect(userState).toEqual({ avatar: mockUser.avatar, isOnboard: true, + canEnablePWAGuide: false, + hasConversation: false, canEnableTrace: false, preference: mockPreference, settings: mockUser.settings, diff --git a/src/services/user/client.ts b/src/services/user/client.ts index bddc5479d9b1..36e99870d6fa 100644 --- a/src/services/user/client.ts +++ b/src/services/user/client.ts @@ -1,6 +1,7 @@ import { DeepPartial } from 'utility-types'; import { MessageModel } from '@/database/client/models/message'; +import { SessionModel } from '@/database/client/models/session'; import { UserModel } from '@/database/client/models/user'; import { GlobalSettings } from '@/types/settings'; import { UserInitializationState, UserPreference } from '@/types/user'; @@ -18,10 +19,13 @@ export class ClientService implements IUserService { async getUserState(): Promise { const user = await UserModel.getUser(); const messageCount = await MessageModel.count(); + const sessionCount = await SessionModel.count(); return { avatar: user.avatar, + canEnablePWAGuide: messageCount >= 2, canEnableTrace: messageCount >= 4, + hasConversation: messageCount > 0 || sessionCount > 0, isOnboard: true, preference: await this.preferenceStorage.getFromLocalStorage(), settings: user.settings as GlobalSettings, diff --git a/src/services/user/type.ts b/src/services/user/type.ts index 18d2c23fbdc9..dfad022ea919 100644 --- a/src/services/user/type.ts +++ b/src/services/user/type.ts @@ -6,7 +6,6 @@ import { UserInitializationState, UserPreference } from '@/types/user'; export interface IUserService { getUserState: () => Promise; resetUserSettings: () => Promise; - updateAvatar: (avatar: string) => Promise; updatePreference: (preference: UserPreference) => Promise; updateUserSettings: (patch: DeepPartial) => Promise; } diff --git a/src/store/user/slices/auth/selectors.ts b/src/store/user/slices/auth/selectors.ts index 4c147a3752c8..e02c7700b150 100644 --- a/src/store/user/slices/auth/selectors.ts +++ b/src/store/user/slices/auth/selectors.ts @@ -41,6 +41,7 @@ const isLogin = (s: UserStore) => { }; export const authSelectors = { + isLoaded: (s: UserStore) => s.isLoaded, isLogin, isLoginWithAuth: (s: UserStore) => s.isSignedIn, isLoginWithClerk: (s: UserStore): boolean => (s.isSignedIn && enableClerk) || false, diff --git a/src/store/user/slices/common/action.test.ts b/src/store/user/slices/common/action.test.ts index a557afec856e..41344e3c5801 100644 --- a/src/store/user/slices/common/action.test.ts +++ b/src/store/user/slices/common/action.test.ts @@ -5,6 +5,7 @@ import { withSWR } from '~test-utils'; import { DEFAULT_PREFERENCE } from '@/const/user'; import { userService } from '@/services/user'; +import { ClientService } from '@/services/user/client'; import { useUserStore } from '@/store/user'; import { preferenceSelectors } from '@/store/user/selectors'; import { GlobalServerConfig } from '@/types/serverConfig'; @@ -36,7 +37,7 @@ describe('createCommonSlice', () => { const avatar = 'new-avatar'; const spyOn = vi.spyOn(result.current, 'refreshUserState'); - const updateAvatarSpy = vi.spyOn(userService, 'updateAvatar'); + const updateAvatarSpy = vi.spyOn(ClientService.prototype, 'updateAvatar'); await act(async () => { await result.current.updateAvatar(avatar); diff --git a/src/store/user/slices/common/action.ts b/src/store/user/slices/common/action.ts index 4f40f3079778..da42a92b1672 100644 --- a/src/store/user/slices/common/action.ts +++ b/src/store/user/slices/common/action.ts @@ -4,6 +4,7 @@ import type { StateCreator } from 'zustand/vanilla'; import { DEFAULT_PREFERENCE } from '@/const/user'; import { userService } from '@/services/user'; +import { ClientService } from '@/services/user/client'; import type { UserStore } from '@/store/user'; import type { GlobalServerConfig } from '@/types/serverConfig'; import type { GlobalSettings } from '@/types/settings'; @@ -45,7 +46,9 @@ export const createCommonSlice: StateCreator< await mutate(GET_USER_STATE_KEY); }, updateAvatar: async (avatar) => { - await userService.updateAvatar(avatar); + const clientService = new ClientService(); + + await clientService.updateAvatar(avatar); await get().refreshUserState(); }, @@ -89,8 +92,12 @@ export const createCommonSlice: StateCreator< { defaultSettings, enabledNextAuth: serverConfig.enabledOAuthSSO, + isOnboard: data.isOnboard, + isShowPWAGuide: data.canEnablePWAGuide, isUserCanEnableTrace: data.canEnableTrace, + isUserHasConversation: data.hasConversation, isUserStateInit: true, + preference, serverLanguageModel: serverConfig.languageModel, settings: data.settings || {}, diff --git a/src/store/user/slices/common/initialState.ts b/src/store/user/slices/common/initialState.ts index 53ae71737eab..201d7cce74d2 100644 --- a/src/store/user/slices/common/initialState.ts +++ b/src/store/user/slices/common/initialState.ts @@ -1,9 +1,15 @@ export interface CommonState { + isOnboard: boolean; + isShowPWAGuide: boolean; isUserCanEnableTrace: boolean; + isUserHasConversation: boolean; isUserStateInit: boolean; } export const initialCommonState: CommonState = { + isOnboard: false, + isShowPWAGuide: false, isUserCanEnableTrace: false, + isUserHasConversation: false, isUserStateInit: false, }; diff --git a/src/types/user/index.ts b/src/types/user/index.ts index 795ed903023a..a7bae9bb13bd 100644 --- a/src/types/user/index.ts +++ b/src/types/user/index.ts @@ -34,7 +34,9 @@ export interface UserPreference { export interface UserInitializationState { avatar?: string; + canEnablePWAGuide?: boolean; canEnableTrace?: boolean; + hasConversation?: boolean; isOnboard?: boolean; preference: UserPreference; settings: DeepPartial;