From 840fc546357e482c6900be8940e75312487d35f3 Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:12:23 +0800 Subject: [PATCH 1/7] fix: require iframe for sealos desktop detection --- .../desktop-sdk/detect-sealos-iframe.test.ts | 111 ++++++++++++++++++ .../desktop-sdk/detect-sealos-iframe.ts | 42 +++++++ 2 files changed, 153 insertions(+) create mode 100644 integrations/sealos/desktop-sdk/detect-sealos-iframe.test.ts create mode 100644 integrations/sealos/desktop-sdk/detect-sealos-iframe.ts diff --git a/integrations/sealos/desktop-sdk/detect-sealos-iframe.test.ts b/integrations/sealos/desktop-sdk/detect-sealos-iframe.test.ts new file mode 100644 index 0000000..db1bc37 --- /dev/null +++ b/integrations/sealos/desktop-sdk/detect-sealos-iframe.test.ts @@ -0,0 +1,111 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { detectSealosIframe } from '@/integrations/sealos/desktop-sdk/detect-sealos-iframe' + +describe('detectSealosIframe', () => { + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('returns false when no window is provided', () => { + expect(detectSealosIframe(undefined)).toBe(false) + }) + + it('returns false when the window has no ancestor origin', () => { + expect(detectSealosIframe({ location: {} })).toBe(false) + }) + + it('returns false for Sealos origins when the window is not framed', () => { + const browserWindow = { + self: 'same-frame', + top: 'same-frame', + location: { + ancestorOrigins: ['https://cloud.sealos.io'], + }, + } + + expect(detectSealosIframe(browserWindow)).toBe(false) + }) + + it('returns true for sealos.io ancestor origins', () => { + expect( + detectSealosIframe({ + self: 'child-frame', + top: 'parent-frame', + location: { + ancestorOrigins: ['https://cloud.sealos.io'], + }, + }), + ).toBe(true) + }) + + it('returns true for sealos.run ancestor origins', () => { + expect( + detectSealosIframe({ + self: 'child-frame', + top: 'parent-frame', + location: { + ancestorOrigins: ['https://workspace.sealos.run'], + }, + }), + ).toBe(true) + }) + + it('returns false for non-Sealos ancestor origins', () => { + expect( + detectSealosIframe({ + location: { + ancestorOrigins: ['https://example.com'], + }, + }), + ).toBe(false) + }) + + it('returns false for spoofed Sealos-like ancestor hosts', () => { + expect( + detectSealosIframe({ + location: { + ancestorOrigins: ['https://sealos.io.evil.com'], + }, + }), + ).toBe(false) + + expect( + detectSealosIframe({ + location: { + ancestorOrigins: ['https://evil-sealos.run.example'], + }, + }), + ).toBe(false) + + expect( + detectSealosIframe({ + location: { + ancestorOrigins: ['https://example.com?next=sealos.io'], + }, + }), + ).toBe(false) + }) + + it('returns false for malformed ancestor origin strings', () => { + expect( + detectSealosIframe({ + location: { + ancestorOrigins: ['not a url with sealos.io'], + }, + }), + ).toBe(false) + }) + + it('returns false when ancestor origin access throws', () => { + const browserWindow = { + location: { + get ancestorOrigins() { + throw new Error('blocked') + }, + }, + } as Parameters[0] + + expect(detectSealosIframe(browserWindow)).toBe(false) + }) +}) diff --git a/integrations/sealos/desktop-sdk/detect-sealos-iframe.ts b/integrations/sealos/desktop-sdk/detect-sealos-iframe.ts new file mode 100644 index 0000000..f9cc7e0 --- /dev/null +++ b/integrations/sealos/desktop-sdk/detect-sealos-iframe.ts @@ -0,0 +1,42 @@ +type BrowserWindowLike = { + self?: unknown + top?: unknown + location?: { + ancestorOrigins?: ArrayLike + } +} + +/** + * Detects whether the current browser frame is embedded by Sealos. + * + * Expected inputs: + * - A browser `window` object, or no value in server/test environments. + * + * Expected outputs: + * - Returns true when the first ancestor origin is a known Sealos host. + * + * Out of scope: + * - Does not initialize the Sealos SDK. + * - Does not authenticate the Fulling user. + */ +export function detectSealosIframe(browserWindow?: BrowserWindowLike): boolean { + if (!browserWindow) return false + + try { + if (browserWindow.self === browserWindow.top) return false + + const ancestorOrigin = browserWindow.location?.ancestorOrigins?.[0] + if (!ancestorOrigin) return false + + const hostname = new URL(ancestorOrigin).hostname + + return ( + hostname === 'sealos.io' || + hostname.endsWith('.sealos.io') || + hostname === 'sealos.run' || + hostname.endsWith('.sealos.run') + ) + } catch { + return false + } +} From d20d8bf80075a180e9aae5e51c8f8078f7832e56 Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:17:26 +0800 Subject: [PATCH 2/7] refactor: keep sealos sdk integration under integrations --- .../desktop-sdk}/get-sealos-session.test.ts | 2 +- .../sealos/desktop-sdk}/get-sealos-session.ts | 0 .../sealos/desktop-sdk}/index.ts | 0 .../sealos/desktop-sdk}/types.ts | 0 integrations/sealos/index.ts | 5 + .../sealos/auth/detect-sealos-iframe.test.ts | 95 ------------------- .../sealos/auth/detect-sealos-iframe.ts | 38 -------- provider/sealos.test.tsx | 2 +- provider/sealos.tsx | 2 +- 9 files changed, 8 insertions(+), 136 deletions(-) rename {lib/platform/integrations/sealos/auth => integrations/sealos/desktop-sdk}/get-sealos-session.test.ts (96%) rename {lib/platform/integrations/sealos/auth => integrations/sealos/desktop-sdk}/get-sealos-session.ts (100%) rename {lib/platform/integrations/sealos/auth => integrations/sealos/desktop-sdk}/index.ts (100%) rename {lib/platform/integrations/sealos/auth => integrations/sealos/desktop-sdk}/types.ts (100%) create mode 100644 integrations/sealos/index.ts delete mode 100644 lib/platform/integrations/sealos/auth/detect-sealos-iframe.test.ts delete mode 100644 lib/platform/integrations/sealos/auth/detect-sealos-iframe.ts diff --git a/lib/platform/integrations/sealos/auth/get-sealos-session.test.ts b/integrations/sealos/desktop-sdk/get-sealos-session.test.ts similarity index 96% rename from lib/platform/integrations/sealos/auth/get-sealos-session.test.ts rename to integrations/sealos/desktop-sdk/get-sealos-session.test.ts index 8dfb88b..9197b2e 100644 --- a/lib/platform/integrations/sealos/auth/get-sealos-session.test.ts +++ b/integrations/sealos/desktop-sdk/get-sealos-session.test.ts @@ -12,7 +12,7 @@ vi.mock('@zjy365/sealos-desktop-sdk/app', () => ({ sealosApp, })) -import { getSealosSession } from '@/lib/platform/integrations/sealos/auth/get-sealos-session' +import { getSealosSession } from '@/integrations/sealos/desktop-sdk/get-sealos-session' describe('getSealosSession', () => { beforeEach(() => { diff --git a/lib/platform/integrations/sealos/auth/get-sealos-session.ts b/integrations/sealos/desktop-sdk/get-sealos-session.ts similarity index 100% rename from lib/platform/integrations/sealos/auth/get-sealos-session.ts rename to integrations/sealos/desktop-sdk/get-sealos-session.ts diff --git a/lib/platform/integrations/sealos/auth/index.ts b/integrations/sealos/desktop-sdk/index.ts similarity index 100% rename from lib/platform/integrations/sealos/auth/index.ts rename to integrations/sealos/desktop-sdk/index.ts diff --git a/lib/platform/integrations/sealos/auth/types.ts b/integrations/sealos/desktop-sdk/types.ts similarity index 100% rename from lib/platform/integrations/sealos/auth/types.ts rename to integrations/sealos/desktop-sdk/types.ts diff --git a/integrations/sealos/index.ts b/integrations/sealos/index.ts new file mode 100644 index 0000000..91d1a1b --- /dev/null +++ b/integrations/sealos/index.ts @@ -0,0 +1,5 @@ +export { + detectSealosIframe, + getSealosSession, +} from './desktop-sdk' +export type { SealosAuthSession, SealosUserInfo } from './desktop-sdk' diff --git a/lib/platform/integrations/sealos/auth/detect-sealos-iframe.test.ts b/lib/platform/integrations/sealos/auth/detect-sealos-iframe.test.ts deleted file mode 100644 index 2003d51..0000000 --- a/lib/platform/integrations/sealos/auth/detect-sealos-iframe.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' - -import { detectSealosIframe } from '@/lib/platform/integrations/sealos/auth/detect-sealos-iframe' - -describe('detectSealosIframe', () => { - afterEach(() => { - vi.unstubAllGlobals() - }) - - it('returns false when no window is provided', () => { - expect(detectSealosIframe(undefined)).toBe(false) - }) - - it('returns false when the window has no ancestor origin', () => { - expect(detectSealosIframe({ location: {} })).toBe(false) - }) - - it('returns true for sealos.io ancestor origins', () => { - expect( - detectSealosIframe({ - location: { - ancestorOrigins: ['https://cloud.sealos.io'], - }, - }), - ).toBe(true) - }) - - it('returns true for sealos.run ancestor origins', () => { - expect( - detectSealosIframe({ - location: { - ancestorOrigins: ['https://workspace.sealos.run'], - }, - }), - ).toBe(true) - }) - - it('returns false for non-Sealos ancestor origins', () => { - expect( - detectSealosIframe({ - location: { - ancestorOrigins: ['https://example.com'], - }, - }), - ).toBe(false) - }) - - it('returns false for spoofed Sealos-like ancestor hosts', () => { - expect( - detectSealosIframe({ - location: { - ancestorOrigins: ['https://sealos.io.evil.com'], - }, - }), - ).toBe(false) - - expect( - detectSealosIframe({ - location: { - ancestorOrigins: ['https://evil-sealos.run.example'], - }, - }), - ).toBe(false) - - expect( - detectSealosIframe({ - location: { - ancestorOrigins: ['https://example.com?next=sealos.io'], - }, - }), - ).toBe(false) - }) - - it('returns false for malformed ancestor origin strings', () => { - expect( - detectSealosIframe({ - location: { - ancestorOrigins: ['not a url with sealos.io'], - }, - }), - ).toBe(false) - }) - - it('returns false when ancestor origin access throws', () => { - const browserWindow = { - location: { - get ancestorOrigins() { - throw new Error('blocked') - }, - }, - } - - expect(detectSealosIframe(browserWindow)).toBe(false) - }) -}) diff --git a/lib/platform/integrations/sealos/auth/detect-sealos-iframe.ts b/lib/platform/integrations/sealos/auth/detect-sealos-iframe.ts deleted file mode 100644 index 427ef5f..0000000 --- a/lib/platform/integrations/sealos/auth/detect-sealos-iframe.ts +++ /dev/null @@ -1,38 +0,0 @@ -type BrowserWindowLike = { - location?: { - ancestorOrigins?: ArrayLike - } -} - -/** - * Detects whether the current browser frame is embedded by Sealos. - * - * Expected inputs: - * - A browser `window` object, or no value in server/test environments. - * - * Expected outputs: - * - Returns true when the first ancestor origin is a known Sealos host. - * - * Out of scope: - * - Does not initialize the Sealos SDK. - * - Does not authenticate the Fulling user. - */ -export function detectSealosIframe(browserWindow?: BrowserWindowLike): boolean { - if (!browserWindow) return false - - try { - const ancestorOrigin = browserWindow.location?.ancestorOrigins?.[0] - if (!ancestorOrigin) return false - - const hostname = new URL(ancestorOrigin).hostname - - return ( - hostname === 'sealos.io' || - hostname.endsWith('.sealos.io') || - hostname === 'sealos.run' || - hostname.endsWith('.sealos.run') - ) - } catch { - return false - } -} diff --git a/provider/sealos.test.tsx b/provider/sealos.test.tsx index 0bd7b35..f55b4c4 100644 --- a/provider/sealos.test.tsx +++ b/provider/sealos.test.tsx @@ -11,7 +11,7 @@ const { detectSealosIframe, getSealosSession } = vi.hoisted(() => ({ getSealosSession: vi.fn(), })) -vi.mock('@/lib/platform/integrations/sealos/auth', () => ({ +vi.mock('@/integrations/sealos/desktop-sdk', () => ({ detectSealosIframe, getSealosSession, })) diff --git a/provider/sealos.tsx b/provider/sealos.tsx index e6807b4..a8f763e 100644 --- a/provider/sealos.tsx +++ b/provider/sealos.tsx @@ -6,7 +6,7 @@ import { detectSealosIframe, getSealosSession, type SealosUserInfo, -} from '@/lib/platform/integrations/sealos/auth'; +} from '@/integrations/sealos/desktop-sdk'; let sealosInitPromise: Promise | null = null; From 1bafe206b0bbaf99ef0cd1c98a9ab1b9e20f7ea1 Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:21:37 +0800 Subject: [PATCH 3/7] refactor: remove sealos integration compatibility barrel --- integrations/sealos/index.ts | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 integrations/sealos/index.ts diff --git a/integrations/sealos/index.ts b/integrations/sealos/index.ts deleted file mode 100644 index 91d1a1b..0000000 --- a/integrations/sealos/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - detectSealosIframe, - getSealosSession, -} from './desktop-sdk' -export type { SealosAuthSession, SealosUserInfo } from './desktop-sdk' From 49763c45119d1995c7da0d63b7af54e4c4971e09 Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:28:11 +0800 Subject: [PATCH 4/7] test: cover framed sealos origin rejection --- .../sealos/desktop-sdk/detect-sealos-iframe.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/integrations/sealos/desktop-sdk/detect-sealos-iframe.test.ts b/integrations/sealos/desktop-sdk/detect-sealos-iframe.test.ts index db1bc37..79a530a 100644 --- a/integrations/sealos/desktop-sdk/detect-sealos-iframe.test.ts +++ b/integrations/sealos/desktop-sdk/detect-sealos-iframe.test.ts @@ -54,6 +54,8 @@ describe('detectSealosIframe', () => { it('returns false for non-Sealos ancestor origins', () => { expect( detectSealosIframe({ + self: 'child-frame', + top: 'parent-frame', location: { ancestorOrigins: ['https://example.com'], }, @@ -64,6 +66,8 @@ describe('detectSealosIframe', () => { it('returns false for spoofed Sealos-like ancestor hosts', () => { expect( detectSealosIframe({ + self: 'child-frame', + top: 'parent-frame', location: { ancestorOrigins: ['https://sealos.io.evil.com'], }, @@ -72,6 +76,8 @@ describe('detectSealosIframe', () => { expect( detectSealosIframe({ + self: 'child-frame', + top: 'parent-frame', location: { ancestorOrigins: ['https://evil-sealos.run.example'], }, @@ -80,6 +86,8 @@ describe('detectSealosIframe', () => { expect( detectSealosIframe({ + self: 'child-frame', + top: 'parent-frame', location: { ancestorOrigins: ['https://example.com?next=sealos.io'], }, @@ -90,6 +98,8 @@ describe('detectSealosIframe', () => { it('returns false for malformed ancestor origin strings', () => { expect( detectSealosIframe({ + self: 'child-frame', + top: 'parent-frame', location: { ancestorOrigins: ['not a url with sealos.io'], }, @@ -99,6 +109,8 @@ describe('detectSealosIframe', () => { it('returns false when ancestor origin access throws', () => { const browserWindow = { + self: 'child-frame', + top: 'parent-frame', location: { get ancestorOrigins() { throw new Error('blocked') From 2e9a65514f206630d8ac9022ffea65aedfe8f0e7 Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:45:44 +0800 Subject: [PATCH 5/7] refactor: remove provider layer --- app/(landing)/_components/landing-client.tsx | 85 +-------- app/layout.tsx | 3 +- components/dialog/settings-dialog.tsx | 3 +- components/home-page.tsx | 55 +----- provider/providers.tsx | 42 ----- provider/sealos.test.tsx | 185 ------------------- provider/sealos.tsx | 136 -------------- 7 files changed, 15 insertions(+), 494 deletions(-) delete mode 100644 provider/providers.tsx delete mode 100644 provider/sealos.test.tsx delete mode 100644 provider/sealos.tsx diff --git a/app/(landing)/_components/landing-client.tsx b/app/(landing)/_components/landing-client.tsx index 50f522d..12ab448 100644 --- a/app/(landing)/_components/landing-client.tsx +++ b/app/(landing)/_components/landing-client.tsx @@ -1,11 +1,9 @@ 'use client'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useSession } from 'next-auth/react'; -import { authenticateWithSealos } from '@/lib/actions/sealos-auth'; -import { useSealos } from '@/provider/sealos'; import { HeroSection } from './hero-section'; import { LandingHeader } from './landing-header'; @@ -30,102 +28,39 @@ interface LandingClientProps { export function LandingClient({ starCount }: LandingClientProps) { const router = useRouter(); const { status } = useSession(); - const { isInitialized, isLoading, isSealos, sealosToken, sealosKubeconfig } = useSealos(); - const [isAuthenticating, setIsAuthenticating] = useState(false); const [authError, setAuthError] = useState(null); - const hasAttemptedAuth = useRef(false); // Prevent duplicate auth attempts - - // Auto-trigger authentication in Sealos environment - useEffect(() => { - // Wait for Sealos initialization - if (!isInitialized || isLoading) return; - - // Already authenticated, no need to auth again - if (status === 'authenticated') return; - - // Not in Sealos, don't auto-authenticate - if (!isSealos) return; - - // Prevent duplicate attempts - if (hasAttemptedAuth.current) return; - hasAttemptedAuth.current = true; - - // Check credentials - if (!sealosToken || !sealosKubeconfig) { - queueMicrotask(() => { - setAuthError('Missing Sealos credentials'); - }); - return; - } - - // Trigger authentication - queueMicrotask(() => { - setIsAuthenticating(true); - }); - - authenticateWithSealos(sealosToken, sealosKubeconfig) - .then((result) => { - if (result.success) { - // Authentication successful - don't auto-redirect, let user click - setIsAuthenticating(false); - router.refresh(); // Refresh session - } else { - setAuthError(result.error || 'Authentication failed'); - setIsAuthenticating(false); - hasAttemptedAuth.current = false; // Allow retry - } - }) - .catch((error) => { - setAuthError(error instanceof Error ? error.message : 'Unknown error'); - setIsAuthenticating(false); - hasAttemptedAuth.current = false; // Allow retry - }); - }, [isInitialized, isLoading, status, isSealos, sealosToken, sealosKubeconfig, router]); // Handle Get Started button click const handleGetStarted = useCallback(() => { setAuthError(null); - // Authenticated users go to projects if (status === 'authenticated') { router.push('/projects'); return; } - // Non-Sealos environment - go to login - if (!isSealos) { - router.push('/login'); - return; - } - - // Sealos environment - retry authentication - hasAttemptedAuth.current = false; - }, [status, isSealos, router]); + router.push('/login'); + }, [status, router]); // Handle Sign In button click const handleSignIn = useCallback(() => { if (status === 'authenticated') { router.push('/projects'); - } else if (isSealos) { - // Sealos environment - retry auth - hasAttemptedAuth.current = false; - setAuthError(null); - } else { - router.push('/login'); + return; } - }, [status, isSealos, router]); - // Button state and text logic - const isInitializing = !isInitialized || isLoading; - const isButtonLoading = isInitializing || isAuthenticating; - const shouldShowGoToProjects = isSealos || status === 'authenticated'; + router.push('/login'); + }, [status, router]); + + const isButtonLoading = status === 'loading'; + const shouldShowGoToProjects = status === 'authenticated'; return (
- {children} + {children} (defaultTab); // System Prompt state diff --git a/components/home-page.tsx b/components/home-page.tsx index 91e8bde..9f8232d 100644 --- a/components/home-page.tsx +++ b/components/home-page.tsx @@ -7,8 +7,6 @@ import { useRouter } from 'next/navigation'; import { useSession } from 'next-auth/react'; import { Button } from '@/components/ui/button'; -import { authenticateWithSealos } from '@/lib/actions/sealos-auth'; -import { useSealos } from '@/provider/sealos'; /** * Home page client component with unified rendering. @@ -22,9 +20,7 @@ import { useSealos } from '@/provider/sealos'; export function HomePage() { const router = useRouter(); const { status } = useSession(); - const { isInitialized, isLoading, isSealos, sealosToken, sealosKubeconfig } = useSealos(); - const [isAuthenticating, setIsAuthenticating] = useState(false); const [authError, setAuthError] = useState(null); // Determine button action based on environment and auth status @@ -38,35 +34,7 @@ export function HomePage() { return; } - // Non-Sealos environment - go to login - if (!isSealos) { - router.push('/login'); - return; - } - - // Sealos environment + unauthenticated - trigger Sealos auth - if (!sealosToken || !sealosKubeconfig) { - setAuthError('Missing Sealos credentials'); - return; - } - - setIsAuthenticating(true); - - try { - const result = await authenticateWithSealos(sealosToken, sealosKubeconfig); - - if (result.success) { - // Authentication successful - redirect to projects - router.push('/projects'); - router.refresh(); - } else { - setAuthError(result.error || 'Authentication failed'); - setIsAuthenticating(false); - } - } catch (error) { - setAuthError(error instanceof Error ? error.message : 'Unknown error'); - setIsAuthenticating(false); - } + router.push('/login'); }; const getButtonText = useCallback(() => { @@ -76,9 +44,7 @@ export function HomePage() { return 'Get Started'; }, [status]); - // Show minimal loading during initialization - const isInitializing = !isInitialized || isLoading; - const isButtonDisabled = isInitializing || isAuthenticating; + const isButtonDisabled = status === 'loading'; return ( <> @@ -135,7 +101,7 @@ export function HomePage() { size="lg" onClick={handleGetStarted} disabled={isButtonDisabled} - aria-busy={isAuthenticating} + aria-busy={status === 'loading'} className="w-48" > {getButtonText()} @@ -153,21 +119,6 @@ export function HomePage() {
- - {/* Authentication overlay - shown during Sealos auth process */} - {isAuthenticating && ( -
-
-
-

Authenticating with Sealos...

-
-
- )} ); } diff --git a/provider/providers.tsx b/provider/providers.tsx deleted file mode 100644 index 1ea3e2b..0000000 --- a/provider/providers.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import { SessionProvider } from 'next-auth/react'; -import { ThemeProvider } from 'next-themes'; - -import { SealosProvider } from './sealos'; - -export function Providers({ children }: { children: React.ReactNode }) { - const [queryClient] = useState( - () => - new QueryClient({ - defaultOptions: { - queries: { - staleTime: 5 * 1000, // 5 seconds - retry: 2, - refetchOnWindowFocus: false, - }, - }, - }), - ); - - return ( - - - - - {children} - - - {process.env.NODE_ENV === 'development' && } - - - ); -} diff --git a/provider/sealos.test.tsx b/provider/sealos.test.tsx deleted file mode 100644 index f55b4c4..0000000 --- a/provider/sealos.test.tsx +++ /dev/null @@ -1,185 +0,0 @@ -/** - * @vitest-environment jsdom - */ - -import { act } from 'react' -import { createRoot, type Root } from 'react-dom/client' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -const { detectSealosIframe, getSealosSession } = vi.hoisted(() => ({ - detectSealosIframe: vi.fn(), - getSealosSession: vi.fn(), -})) - -vi.mock('@/integrations/sealos/desktop-sdk', () => ({ - detectSealosIframe, - getSealosSession, -})) - -import { SealosProvider, useSealos } from '@/provider/sealos' - -function Deferred() { - let resolve!: (value: T) => void - let reject!: (error: unknown) => void - const promise = new Promise((promiseResolve, promiseReject) => { - resolve = promiseResolve - reject = promiseReject - }) - - return { promise, resolve, reject } -} - -function createSession(input: { - token: string - namespaceId: string - cleanup: () => void -}) { - return { - token: input.token, - kubeconfig: 'apiVersion: v1', - user: { - id: 'user-id', - name: 'sealos-user', - avatar: '', - k8sUsername: 'k8s-user', - nsid: input.namespaceId, - }, - namespaceId: input.namespaceId, - cleanup: input.cleanup, - } -} - -function SealosStateView() { - const sealos = useSealos() - - return ( - - {JSON.stringify({ - isInitialized: sealos.isInitialized, - isLoading: sealos.isLoading, - isSealos: sealos.isSealos, - sealosToken: sealos.sealosToken, - sealosNs: sealos.sealosNs, - })} - - ) -} - -describe('SealosProvider', () => { - let container: HTMLDivElement - let root: Root - - beforeEach(() => { - vi.clearAllMocks() - detectSealosIframe.mockReturnValue(true) - container = document.createElement('div') - document.body.appendChild(container) - root = createRoot(container) - }) - - afterEach(() => { - act(() => { - root.unmount() - }) - container.remove() - }) - - it('cleans up stale sessions and still initializes after Strict Mode effect replay', async () => { - const firstSession = Deferred>() - const secondSession = Deferred>() - const firstCleanup = vi.fn() - const secondCleanup = vi.fn() - - getSealosSession - .mockReturnValueOnce(firstSession.promise) - .mockReturnValueOnce(secondSession.promise) - - act(() => { - root.render( - - - , - ) - }) - - act(() => { - root.unmount() - }) - - root = createRoot(container) - act(() => { - root.render( - - - , - ) - }) - - await act(async () => { - firstSession.resolve( - createSession({ - token: 'stale-token', - namespaceId: 'stale-ns', - cleanup: firstCleanup, - }), - ) - }) - - expect(firstCleanup).toHaveBeenCalledTimes(1) - - await act(async () => { - secondSession.resolve( - createSession({ - token: 'active-token', - namespaceId: 'active-ns', - cleanup: secondCleanup, - }), - ) - }) - - expect(getSealosSession).toHaveBeenCalledTimes(2) - expect(secondCleanup).not.toHaveBeenCalled() - - const state = JSON.parse( - container.querySelector('[data-testid="sealos-state"]')?.textContent ?? '{}', - ) - - expect(state).toEqual({ - isInitialized: true, - isLoading: false, - isSealos: true, - sealosToken: 'active-token', - sealosNs: 'active-ns', - }) - - act(() => { - root.unmount() - }) - - expect(secondCleanup).toHaveBeenCalledTimes(1) - }) - - it('does not set fallback state after unmount when session loading fails', async () => { - const pendingSession = Deferred>() - getSealosSession.mockReturnValueOnce(pendingSession.promise) - - act(() => { - root.render( - - - , - ) - }) - - act(() => { - root.unmount() - }) - - await act(async () => { - pendingSession.reject(new Error('session unavailable')) - }) - - expect(getSealosSession).toHaveBeenCalledTimes(1) - expect(container.textContent).toBe('') - }) -}) diff --git a/provider/sealos.tsx b/provider/sealos.tsx deleted file mode 100644 index a8f763e..0000000 --- a/provider/sealos.tsx +++ /dev/null @@ -1,136 +0,0 @@ -'use client'; - -import React, { createContext, useContext, useEffect, useRef, useState } from 'react'; - -import { - detectSealosIframe, - getSealosSession, - type SealosUserInfo, -} from '@/integrations/sealos/desktop-sdk'; - -let sealosInitPromise: Promise | null = null; - -interface SealosContextType { - isInitialized: boolean; - isLoading: boolean; - isSealos: boolean; - error: string | null; - sealosToken: string | null; - sealosKubeconfig: string | null; - sealosUser: SealosUserInfo | null; - sealosNs: string | null; -} - -const SealosContext = createContext(null); - -export function SealosProvider({ children }: { children: React.ReactNode }) { - const [state, setState] = useState({ - isInitialized: false, - isLoading: true, - isSealos: false, - error: null, - sealosToken: null, - sealosKubeconfig: null, - sealosUser: null, - sealosNs: null, - }); - - const cleanupRef = useRef<(() => void) | null>(null); - - useEffect(() => { - let disposed = false; - - const initializeSealos = async () => { - try { - setState((prev) => ({ ...prev, isLoading: true, error: null })); - - const isInSealosIframe = detectSealosIframe(window); - - if (!isInSealosIframe) { - // Not in Sealos environment, skip SDK initialization - console.info('Not in Sealos iframe environment'); - setState({ - isInitialized: true, - isLoading: false, - isSealos: false, - error: null, - sealosToken: null, - sealosKubeconfig: null, - sealosUser: null, - sealosNs: null, - }); - return; - } - - console.info('Detected Sealos iframe environment, initializing SDK...'); - console.info('Getting Sealos session...'); - const sealosSession = await getSealosSession(); - - if (disposed) { - sealosSession.cleanup(); - return; - } - - setState({ - isInitialized: true, - isLoading: false, - isSealos: true, - error: null, - sealosToken: sealosSession.token, - sealosKubeconfig: sealosSession.kubeconfig, - sealosUser: sealosSession.user, - sealosNs: sealosSession.namespaceId, - }); - - cleanupRef.current = sealosSession.cleanup; - } catch (error) { - console.info( - 'Sealos SDK initialization failed, falling back to non-Sealos mode:', - error - ); - if (disposed) return; - - setState((prev) => ({ - ...prev, - isInitialized: true, - isLoading: false, - isSealos: false, - error: error instanceof Error ? error.message : 'Unknown error', - })); - } - }; - - sealosInitPromise = initializeSealos().finally(() => { - console.info('##### sealos app and sealos info init completed #####'); - }); - - return () => { - disposed = true; - cleanupRef.current?.(); - cleanupRef.current = null; - }; - }, []); - - return {children}; -} - -export function useSealos() { - const context = useContext(SealosContext); - if (!context) { - throw new Error('useSealos must be used within a SealosProvider'); - } - return context; -} - -export async function waitForSealosInit(): Promise { - if (!sealosInitPromise) { - // if not initialized, return a resolved promise (maybe server-side rendering or test environment) - console.warn('Sealos initialization promise not found, resolving immediately'); - return Promise.resolve(); - } - return sealosInitPromise.catch((error) => { - console.error('Sealos initialization failed:', error); - // even if initialization fails, do not throw an error, let the application continue running - return Promise.resolve(); - }); -} From d4ac6287cf072951f525ec71fdf190da67c593ee Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:51:47 +0800 Subject: [PATCH 6/7] fix: avoid session hook without provider --- app/(landing)/_components/landing-client.tsx | 23 +++++--------------- components/home-page.tsx | 20 +++-------------- 2 files changed, 8 insertions(+), 35 deletions(-) diff --git a/app/(landing)/_components/landing-client.tsx b/app/(landing)/_components/landing-client.tsx index 12ab448..0934b88 100644 --- a/app/(landing)/_components/landing-client.tsx +++ b/app/(landing)/_components/landing-client.tsx @@ -2,7 +2,6 @@ import { useCallback, useState } from 'react'; import { useRouter } from 'next/navigation'; -import { useSession } from 'next-auth/react'; import { HeroSection } from './hero-section'; @@ -27,39 +26,27 @@ interface LandingClientProps { */ export function LandingClient({ starCount }: LandingClientProps) { const router = useRouter(); - const { status } = useSession(); const [authError, setAuthError] = useState(null); // Handle Get Started button click const handleGetStarted = useCallback(() => { setAuthError(null); - - if (status === 'authenticated') { - router.push('/projects'); - return; - } - router.push('/login'); - }, [status, router]); + }, [router]); // Handle Sign In button click const handleSignIn = useCallback(() => { - if (status === 'authenticated') { - router.push('/projects'); - return; - } - router.push('/login'); - }, [status, router]); + }, [router]); - const isButtonLoading = status === 'loading'; - const shouldShowGoToProjects = status === 'authenticated'; + const isButtonLoading = false; + const shouldShowGoToProjects = false; return (
(null); @@ -27,24 +25,12 @@ export function HomePage() { const handleGetStarted = async () => { // Clear previous errors on retry setAuthError(null); - - // Already authenticated - go to projects - if (status === 'authenticated') { - router.push('/projects'); - return; - } - router.push('/login'); }; - const getButtonText = useCallback(() => { - if (status === 'authenticated') { - return 'Go to Projects'; - } - return 'Get Started'; - }, [status]); + const getButtonText = useCallback(() => 'Get Started', []); - const isButtonDisabled = status === 'loading'; + const isButtonDisabled = false; return ( <> @@ -101,7 +87,7 @@ export function HomePage() { size="lg" onClick={handleGetStarted} disabled={isButtonDisabled} - aria-busy={status === 'loading'} + aria-busy={false} className="w-48" > {getButtonText()} From 9b2d621fb6b65be6584a4886e07e7ab5223e641b Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:57:46 +0800 Subject: [PATCH 7/7] fix: sort landing client imports --- app/(landing)/_components/landing-client.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/(landing)/_components/landing-client.tsx b/app/(landing)/_components/landing-client.tsx index 0934b88..9a16e83 100644 --- a/app/(landing)/_components/landing-client.tsx +++ b/app/(landing)/_components/landing-client.tsx @@ -3,7 +3,6 @@ import { useCallback, useState } from 'react'; import { useRouter } from 'next/navigation'; - import { HeroSection } from './hero-section'; import { LandingHeader } from './landing-header'; import { TerminalDemo } from './terminal-demo';