diff --git a/web/app/(commonLayout)/layout-client.tsx b/web/app/(commonLayout)/layout-client.tsx
deleted file mode 100644
index b1025c5fd8..0000000000
--- a/web/app/(commonLayout)/layout-client.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-'use client'
-
-import type { ReactNode } from 'react'
-import { AppInitializer } from '@/app/components/app-initializer'
-import AmplitudeProvider from '@/app/components/base/amplitude'
-import GotoAnything from '@/app/components/goto-anything'
-import Header from '@/app/components/header'
-import HeaderWrapper from '@/app/components/header/header-wrapper'
-import ReadmePanel from '@/app/components/plugins/readme-panel'
-import { AppContextProvider } from '@/context/app-context'
-import { EventEmitterContextProvider } from '@/context/event-emitter'
-import { ModalContextProvider } from '@/context/modal-context'
-import { ProviderContextProvider } from '@/context/provider-context'
-import PartnerStack from '../components/billing/partner-stack'
-
-export const CommonLayoutClient = ({ children }: { children: ReactNode }) => {
- return (
- <>
-
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
-
- >
- )
-}
diff --git a/web/app/(commonLayout)/layout.tsx b/web/app/(commonLayout)/layout.tsx
index 348db5f81f..a0ccde957d 100644
--- a/web/app/(commonLayout)/layout.tsx
+++ b/web/app/(commonLayout)/layout.tsx
@@ -1,46 +1,45 @@
import type { ReactNode } from 'react'
-import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
+import * as React from 'react'
+import { AppInitializer } from '@/app/components/app-initializer'
+import AmplitudeProvider from '@/app/components/base/amplitude'
import GA, { GaType } from '@/app/components/base/ga'
import Zendesk from '@/app/components/base/zendesk'
-import { getQueryClientServer } from '@/context/query-client-server'
-import { serverFetchWithAuth } from '@/utils/ssr-fetch'
-import { CommonLayoutClient } from './layout-client'
-
-const IS_DEV = process.env.NODE_ENV === 'development'
-
-async function fetchUserProfileForSSR() {
- const { data: profile, headers } = await serverFetchWithAuth('/account/profile')
- return {
- profile,
- meta: {
- currentVersion: headers.get('x-version'),
- currentEnv: IS_DEV ? 'DEVELOPMENT' : headers.get('x-env'),
- },
- }
-}
-
-export default async function CommonLayout({ children }: { children: ReactNode }) {
- const queryClient = getQueryClientServer()
-
- await Promise.all([
- queryClient.prefetchQuery({
- queryKey: ['common', 'user-profile'],
- queryFn: fetchUserProfileForSSR,
- }),
- queryClient.prefetchQuery({
- queryKey: ['common', 'current-workspace'],
- queryFn: async () => {
- const { data } = await serverFetchWithAuth('/workspaces/current', 'POST', {})
- return data
- },
- }),
- ])
+import GotoAnything from '@/app/components/goto-anything'
+import Header from '@/app/components/header'
+import HeaderWrapper from '@/app/components/header/header-wrapper'
+import ReadmePanel from '@/app/components/plugins/readme-panel'
+import { AppContextProvider } from '@/context/app-context'
+import { EventEmitterContextProvider } from '@/context/event-emitter'
+import { ModalContextProvider } from '@/context/modal-context'
+import { ProviderContextProvider } from '@/context/provider-context'
+import PartnerStack from '../components/billing/partner-stack'
+import Splash from '../components/splash'
+const Layout = ({ children }: { children: ReactNode }) => {
return (
-
+ <>
- {children}
-
-
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+
+ >
)
}
+export default Layout
diff --git a/web/app/(commonLayout)/loading.tsx b/web/app/(commonLayout)/loading.tsx
deleted file mode 100644
index 6a975ef7d3..0000000000
--- a/web/app/(commonLayout)/loading.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import '@/app/components/base/loading/style.css'
-
-export default function CommonLayoutLoading() {
- return (
-
-
-
- )
-}
diff --git a/web/app/components/app-initializer.tsx b/web/app/components/app-initializer.tsx
index fb6ac1c6da..3410ecbe9a 100644
--- a/web/app/components/app-initializer.tsx
+++ b/web/app/components/app-initializer.tsx
@@ -2,15 +2,15 @@
import type { ReactNode } from 'react'
import Cookies from 'js-cookie'
-import { useRouter, useSearchParams } from 'next/navigation'
+import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { parseAsString, useQueryState } from 'nuqs'
-import { useEffect, useReducer, useRef } from 'react'
+import { useCallback, useEffect, useState } from 'react'
import {
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
} from '@/app/education-apply/constants'
-import { useSetupStatusQuery } from '@/context/global-public-context'
import { sendGAEvent } from '@/utils/gtag'
+import { fetchSetupStatusWithCache } from '@/utils/setup-status'
import { resolvePostLoginRedirect } from '../signin/utils/post-login-redirect'
import { trackEvent } from './base/amplitude'
@@ -23,68 +23,80 @@ export const AppInitializer = ({
}: AppInitializerProps) => {
const router = useRouter()
const searchParams = useSearchParams()
- const [init, markInit] = useReducer(() => true, false)
- const { data: setupStatus } = useSetupStatusQuery()
+ // Tokens are now stored in cookies, no need to check localStorage
+ const pathname = usePathname()
+ const [init, setInit] = useState(false)
const [oauthNewUser, setOauthNewUser] = useQueryState(
'oauth_new_user',
parseAsString.withOptions({ history: 'replace' }),
)
- const oauthTrackedRef = useRef(false)
+
+ const isSetupFinished = useCallback(async () => {
+ try {
+ const setUpStatus = await fetchSetupStatusWithCache()
+ return setUpStatus.step === 'finished'
+ }
+ catch (error) {
+ console.error(error)
+ return false
+ }
+ }, [])
useEffect(() => {
- if (oauthNewUser !== 'true' || oauthTrackedRef.current)
- return
- oauthTrackedRef.current = true
+ (async () => {
+ const action = searchParams.get('action')
+
+ if (oauthNewUser === 'true') {
+ let utmInfo = null
+ const utmInfoStr = Cookies.get('utm_info')
+ if (utmInfoStr) {
+ try {
+ utmInfo = JSON.parse(utmInfoStr)
+ }
+ catch (e) {
+ console.error('Failed to parse utm_info cookie:', e)
+ }
+ }
+
+ // Track registration event with UTM params
+ trackEvent(utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success', {
+ method: 'oauth',
+ ...utmInfo,
+ })
+
+ sendGAEvent(utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success', {
+ method: 'oauth',
+ ...utmInfo,
+ })
+
+ // Clean up: remove utm_info cookie and URL params
+ Cookies.remove('utm_info')
+ setOauthNewUser(null)
+ }
+
+ if (action === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
+ localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes')
- let utmInfo = null
- const utmInfoStr = Cookies.get('utm_info')
- if (utmInfoStr) {
try {
- utmInfo = JSON.parse(utmInfoStr)
+ const isFinished = await isSetupFinished()
+ if (!isFinished) {
+ router.replace('/install')
+ return
+ }
+
+ const redirectUrl = resolvePostLoginRedirect(searchParams)
+ if (redirectUrl) {
+ location.replace(redirectUrl)
+ return
+ }
+
+ setInit(true)
}
- catch (e) {
- console.error('Failed to parse utm_info cookie:', e)
+ catch {
+ router.replace('/signin')
}
- }
-
- trackEvent(utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success', {
- method: 'oauth',
- ...utmInfo,
- })
-
- sendGAEvent(utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success', {
- method: 'oauth',
- ...utmInfo,
- })
-
- Cookies.remove('utm_info')
- // eslint-disable-next-line react-hooks-extra/no-direct-set-state-in-use-effect -- setOauthNewUser is from nuqs useQueryState, not useState
- setOauthNewUser(null)
- }, [oauthNewUser, setOauthNewUser])
-
- useEffect(() => {
- const action = searchParams.get('action')
- if (action === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
- localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes')
- }, [searchParams])
-
- useEffect(() => {
- if (!setupStatus)
- return
-
- if (setupStatus.step !== 'finished') {
- router.replace('/install')
- return
- }
-
- const redirectUrl = resolvePostLoginRedirect(searchParams)
- if (redirectUrl) {
- location.replace(redirectUrl)
- return
- }
-
- markInit()
- }, [setupStatus, router, searchParams])
+ })()
+ }, [isSetupFinished, router, pathname, searchParams, oauthNewUser, setOauthNewUser])
return init ? children : null
}
diff --git a/web/app/components/billing/partner-stack/use-ps-info.ts b/web/app/components/billing/partner-stack/use-ps-info.ts
index 82de909f6a..51d693f358 100644
--- a/web/app/components/billing/partner-stack/use-ps-info.ts
+++ b/web/app/components/billing/partner-stack/use-ps-info.ts
@@ -23,8 +23,8 @@ const usePSInfo = () => {
setTrue: setBind,
}] = useBoolean(false)
const { mutateAsync } = useBindPartnerStackInfo()
-
- const getDomain = () => globalThis.location?.hostname.replace('cloud', '') ?? ''
+ // Save to top domain. cloud.dify.ai => .dify.ai
+ const domain = globalThis.location.hostname.replace('cloud', '')
const saveOrUpdate = useCallback(() => {
if (!psPartnerKey || !psClickId)
@@ -37,7 +37,7 @@ const usePSInfo = () => {
}), {
expires: PARTNER_STACK_CONFIG.saveCookieDays,
path: '/',
- domain: getDomain(),
+ domain,
})
}, [psPartnerKey, psClickId, isPSChanged])
@@ -56,7 +56,7 @@ const usePSInfo = () => {
shouldRemoveCookie = true
}
if (shouldRemoveCookie)
- Cookies.remove(PARTNER_STACK_CONFIG.cookieName, { path: '/', domain: getDomain() })
+ Cookies.remove(PARTNER_STACK_CONFIG.cookieName, { path: '/', domain })
setBind()
}
}, [psPartnerKey, psClickId, mutateAsync, hasBind, setBind])
diff --git a/web/app/components/splash.tsx b/web/app/components/splash.tsx
new file mode 100644
index 0000000000..e4103e8c93
--- /dev/null
+++ b/web/app/components/splash.tsx
@@ -0,0 +1,21 @@
+'use client'
+import type { FC, PropsWithChildren } from 'react'
+import * as React from 'react'
+import { useIsLogin } from '@/service/use-common'
+import Loading from './base/loading'
+
+const Splash: FC = () => {
+ // would auto redirect to signin page if not logged in
+ const { isLoading, data: loginData } = useIsLogin()
+ const isLoggedIn = loginData?.logged_in
+
+ if (isLoading || !isLoggedIn) {
+ return (
+
+
+
+ )
+ }
+ return null
+}
+export default React.memo(Splash)
diff --git a/web/app/layout.tsx b/web/app/layout.tsx
index a8a1c469d0..199c12e814 100644
--- a/web/app/layout.tsx
+++ b/web/app/layout.tsx
@@ -1,16 +1,13 @@
import type { Viewport } from 'next'
-import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import { Provider as JotaiProvider } from 'jotai'
import { ThemeProvider } from 'next-themes'
import { Instrument_Serif } from 'next/font/google'
import { NuqsAdapter } from 'nuqs/adapters/next/app'
import GlobalPublicStoreProvider from '@/context/global-public-context'
import { TanstackQueryInitializer } from '@/context/query-client'
-import { getQueryClientServer } from '@/context/query-client-server'
import { getLocaleOnServer } from '@/i18n-config/server'
import { DatasetAttr } from '@/types/feature'
import { cn } from '@/utils/classnames'
-import { serverFetch } from '@/utils/ssr-fetch'
import { ToastProvider } from './components/base/toast'
import BrowserInitializer from './components/browser-initializer'
import { ReactScanLoader } from './components/devtools/react-scan/loader'
@@ -42,18 +39,6 @@ const LocaleLayout = async ({
children: React.ReactNode
}) => {
const locale = await getLocaleOnServer()
- const queryClient = getQueryClientServer()
-
- await Promise.all([
- queryClient.prefetchQuery({
- queryKey: ['systemFeatures'],
- queryFn: () => serverFetch('/system-features'),
- }),
- queryClient.prefetchQuery({
- queryKey: ['setupStatus'],
- queryFn: () => serverFetch('/setup'),
- }),
- ])
const datasetMap: Record = {
[DatasetAttr.DATA_API_PREFIX]: process.env.NEXT_PUBLIC_API_PREFIX,
@@ -123,15 +108,13 @@ const LocaleLayout = async ({
-
-
-
-
- {children}
-
-
-
-
+
+
+
+ {children}
+
+
+
diff --git a/web/context/global-public-context.tsx b/web/context/global-public-context.tsx
index e3d6902fd1..3a570fc7ef 100644
--- a/web/context/global-public-context.tsx
+++ b/web/context/global-public-context.tsx
@@ -2,7 +2,6 @@
import type { FC, PropsWithChildren } from 'react'
import type { SystemFeatures } from '@/types/feature'
import { useQuery } from '@tanstack/react-query'
-import { useEffect } from 'react'
import { create } from 'zustand'
import Loading from '@/app/components/base/loading'
import { consoleClient } from '@/service/client'
@@ -23,7 +22,10 @@ const systemFeaturesQueryKey = ['systemFeatures'] as const
const setupStatusQueryKey = ['setupStatus'] as const
async function fetchSystemFeatures() {
- return consoleClient.systemFeatures()
+ const data = await consoleClient.systemFeatures()
+ const { setSystemFeatures } = useGlobalPublicStore.getState()
+ setSystemFeatures({ ...defaultSystemFeatures, ...data })
+ return data
}
export function useSystemFeaturesQuery() {
@@ -49,15 +51,12 @@ export function useSetupStatusQuery() {
const GlobalPublicStoreProvider: FC = ({
children,
}) => {
- const { data, isPending } = useSystemFeaturesQuery()
- useSetupStatusQuery()
+ // Fetch systemFeatures and setupStatus in parallel to reduce waterfall.
+ // setupStatus is prefetched here and cached in localStorage for AppInitializer.
+ const { isPending } = useSystemFeaturesQuery()
- useEffect(() => {
- if (data) {
- const { setSystemFeatures } = useGlobalPublicStore.getState()
- setSystemFeatures({ ...defaultSystemFeatures, ...data })
- }
- }, [data])
+ // Prefetch setupStatus for AppInitializer (result not needed here)
+ useSetupStatusQuery()
if (isPending)
return
diff --git a/web/service/use-common.ts b/web/service/use-common.ts
index 9bed6ae52b..ca0845d95a 100644
--- a/web/service/use-common.ts
+++ b/web/service/use-common.ts
@@ -92,6 +92,8 @@ export const useUserProfile = () => {
},
}
},
+ staleTime: 0,
+ gcTime: 0,
})
}
diff --git a/web/utils/ssr-fetch.ts b/web/utils/ssr-fetch.ts
deleted file mode 100644
index c02192f1b3..0000000000
--- a/web/utils/ssr-fetch.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { cookies } from 'next/headers'
-import { cache } from 'react'
-
-const SSR_API_PREFIX = process.env.NEXT_PUBLIC_API_PREFIX || 'http://localhost:5001/console/api'
-
-export const getAuthHeaders = cache(async () => {
- const cookieStore = await cookies()
- const cookieHeader = cookieStore.getAll().map(c => `${c.name}=${c.value}`).join('; ')
- const csrfToken = cookieStore.get('csrf_token')?.value
- || cookieStore.get('__Host-csrf_token')?.value
- return {
- 'Content-Type': 'application/json',
- 'Cookie': cookieHeader,
- ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}),
- }
-})
-
-type ServerFetchResult = {
- data: T
- headers: Headers
-}
-
-export async function serverFetchWithAuth(
- path: string,
- method = 'GET',
- body?: unknown,
-): Promise> {
- const headers = await getAuthHeaders()
- const res = await fetch(`${SSR_API_PREFIX}${path}`, {
- method,
- headers,
- ...(body ? { body: JSON.stringify(body) } : {}),
- cache: 'no-store',
- })
-
- if (!res.ok)
- throw new Error(`${res.status}`)
-
- const data: T = await res.json()
- return { data, headers: res.headers }
-}
-
-export async function serverFetch(path: string): Promise {
- const res = await fetch(`${SSR_API_PREFIX}${path}`, { cache: 'no-store' })
- if (!res.ok)
- throw new Error(`${res.status}`)
- return res.json()
-}