diff --git a/frontend/__tests__/components/features/device-verify/enterprise-banner.test.tsx b/frontend/__tests__/components/features/device-verify/enterprise-banner.test.tsx
new file mode 100644
index 0000000000..2568601423
--- /dev/null
+++ b/frontend/__tests__/components/features/device-verify/enterprise-banner.test.tsx
@@ -0,0 +1,118 @@
+import { screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import userEvent from "@testing-library/user-event";
+import { renderWithProviders } from "test-utils";
+import { EnterpriseBanner } from "#/components/features/device-verify/enterprise-banner";
+
+const mockCapture = vi.fn();
+vi.mock("posthog-js/react", () => ({
+ usePostHog: () => ({
+ capture: mockCapture,
+ }),
+}));
+
+const { PROJ_USER_JOURNEY_MOCK } = vi.hoisted(() => ({
+ PROJ_USER_JOURNEY_MOCK: vi.fn(() => true),
+}));
+
+vi.mock("#/utils/feature-flags", () => ({
+ PROJ_USER_JOURNEY: () => PROJ_USER_JOURNEY_MOCK(),
+}));
+
+describe("EnterpriseBanner", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ PROJ_USER_JOURNEY_MOCK.mockReturnValue(true);
+ });
+
+ describe("Feature Flag", () => {
+ it("should not render when proj_user_journey feature flag is disabled", () => {
+ PROJ_USER_JOURNEY_MOCK.mockReturnValue(false);
+
+ const { container } = renderWithProviders( );
+
+ expect(container.firstChild).toBeNull();
+ expect(screen.queryByText("ENTERPRISE$TITLE")).not.toBeInTheDocument();
+ });
+
+ it("should render when proj_user_journey feature flag is enabled", () => {
+ PROJ_USER_JOURNEY_MOCK.mockReturnValue(true);
+
+ renderWithProviders( );
+
+ expect(screen.getByText("ENTERPRISE$TITLE")).toBeInTheDocument();
+ });
+ });
+
+ describe("Rendering", () => {
+ it("should render the self-hosted label", () => {
+ renderWithProviders( );
+
+ expect(screen.getByText("ENTERPRISE$SELF_HOSTED")).toBeInTheDocument();
+ });
+
+ it("should render the enterprise title", () => {
+ renderWithProviders( );
+
+ expect(screen.getByText("ENTERPRISE$TITLE")).toBeInTheDocument();
+ });
+
+ it("should render the enterprise description", () => {
+ renderWithProviders( );
+
+ expect(screen.getByText("ENTERPRISE$DESCRIPTION")).toBeInTheDocument();
+ });
+
+ it("should render all four enterprise feature items", () => {
+ renderWithProviders( );
+
+ expect(
+ screen.getByText("ENTERPRISE$FEATURE_DATA_PRIVACY"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("ENTERPRISE$FEATURE_DEPLOYMENT"),
+ ).toBeInTheDocument();
+ expect(screen.getByText("ENTERPRISE$FEATURE_SSO")).toBeInTheDocument();
+ expect(
+ screen.getByText("ENTERPRISE$FEATURE_SUPPORT"),
+ ).toBeInTheDocument();
+ });
+
+ it("should render the learn more link", () => {
+ renderWithProviders( );
+
+ const link = screen.getByRole("link", {
+ name: "ENTERPRISE$LEARN_MORE_ARIA",
+ });
+ expect(link).toBeInTheDocument();
+ expect(link).toHaveTextContent("ENTERPRISE$LEARN_MORE");
+ expect(link).toHaveAttribute("href", "https://openhands.dev/enterprise");
+ expect(link).toHaveAttribute("target", "_blank");
+ expect(link).toHaveAttribute("rel", "noopener noreferrer");
+ });
+ });
+
+ describe("Learn More Link Interaction", () => {
+ it("should capture PostHog event when learn more link is clicked", async () => {
+ const user = userEvent.setup();
+ renderWithProviders( );
+
+ const link = screen.getByRole("link", {
+ name: "ENTERPRISE$LEARN_MORE_ARIA",
+ });
+ await user.click(link);
+
+ expect(mockCapture).toHaveBeenCalledWith("saas_selfhosted_inquiry");
+ });
+
+ it("should have correct href attribute for opening in new tab", () => {
+ renderWithProviders( );
+
+ const link = screen.getByRole("link", {
+ name: "ENTERPRISE$LEARN_MORE_ARIA",
+ });
+ expect(link).toHaveAttribute("href", "https://openhands.dev/enterprise");
+ expect(link).toHaveAttribute("target", "_blank");
+ });
+ });
+});
diff --git a/frontend/__tests__/routes/device-verify.test.tsx b/frontend/__tests__/routes/device-verify.test.tsx
new file mode 100644
index 0000000000..47773ddbf5
--- /dev/null
+++ b/frontend/__tests__/routes/device-verify.test.tsx
@@ -0,0 +1,659 @@
+import { render, screen, waitFor } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import userEvent from "@testing-library/user-event";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { createRoutesStub } from "react-router";
+import DeviceVerify from "#/routes/device-verify";
+
+const { useIsAuthedMock, PROJ_USER_JOURNEY_MOCK } = vi.hoisted(() => ({
+ useIsAuthedMock: vi.fn(() => ({
+ data: false as boolean | undefined,
+ isLoading: false,
+ })),
+ PROJ_USER_JOURNEY_MOCK: vi.fn(() => true),
+}));
+
+vi.mock("#/hooks/query/use-is-authed", () => ({
+ useIsAuthed: () => useIsAuthedMock(),
+}));
+
+vi.mock("posthog-js/react", () => ({
+ usePostHog: () => ({
+ capture: vi.fn(),
+ }),
+}));
+
+vi.mock("#/utils/feature-flags", () => ({
+ PROJ_USER_JOURNEY: () => PROJ_USER_JOURNEY_MOCK(),
+}));
+
+const RouterStub = createRoutesStub([
+ {
+ Component: DeviceVerify,
+ path: "/device-verify",
+ },
+]);
+
+const createWrapper = () => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+ });
+
+ function Wrapper({ children }: { children: React.ReactNode }) {
+ return (
+ {children}
+ );
+ }
+
+ return Wrapper;
+};
+
+describe("DeviceVerify", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.stubGlobal("close", vi.fn());
+ // Mock fetch for API calls
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({}),
+ }),
+ ),
+ );
+ // Enable feature flag by default
+ PROJ_USER_JOURNEY_MOCK.mockReturnValue(true);
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ vi.unstubAllGlobals();
+ });
+
+ describe("Loading State", () => {
+ it("should show loading spinner while checking authentication", async () => {
+ useIsAuthedMock.mockReturnValue({
+ data: undefined,
+ isLoading: true,
+ });
+
+ render( , {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => {
+ const spinner = document.querySelector(".animate-spin");
+ expect(spinner).toBeInTheDocument();
+ });
+
+ expect(screen.getByText("DEVICE$PROCESSING")).toBeInTheDocument();
+ });
+ });
+
+ describe("Not Authenticated State", () => {
+ it("should show authentication required message when not authenticated", async () => {
+ useIsAuthedMock.mockReturnValue({
+ data: false,
+ isLoading: false,
+ });
+
+ render( , {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("DEVICE$AUTH_REQUIRED")).toBeInTheDocument();
+ });
+
+ expect(screen.getByText("DEVICE$SIGN_IN_PROMPT")).toBeInTheDocument();
+ });
+ });
+
+ describe("Authenticated without User Code", () => {
+ it("should show manual code entry form when authenticated but no code in URL", async () => {
+ useIsAuthedMock.mockReturnValue({
+ data: true,
+ isLoading: false,
+ });
+
+ render( , {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("DEVICE$AUTHORIZATION_TITLE"),
+ ).toBeInTheDocument();
+ });
+
+ expect(screen.getByText("DEVICE$ENTER_CODE_PROMPT")).toBeInTheDocument();
+ expect(screen.getByLabelText("DEVICE$CODE_INPUT_LABEL")).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: "DEVICE$CONTINUE" }),
+ ).toBeInTheDocument();
+ });
+
+ it("should submit manually entered code", async () => {
+ const user = userEvent.setup();
+ useIsAuthedMock.mockReturnValue({
+ data: true,
+ isLoading: false,
+ });
+
+ const mockFetch = vi.fn(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({}),
+ }),
+ );
+ vi.stubGlobal("fetch", mockFetch);
+
+ render( , {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => {
+ expect(screen.getByLabelText("DEVICE$CODE_INPUT_LABEL")).toBeInTheDocument();
+ });
+
+ const input = screen.getByLabelText("DEVICE$CODE_INPUT_LABEL");
+ await user.type(input, "TESTCODE");
+
+ const submitButton = screen.getByRole("button", {
+ name: "DEVICE$CONTINUE",
+ });
+ await user.click(submitButton);
+
+ await waitFor(() => {
+ expect(mockFetch).toHaveBeenCalledWith(
+ "/oauth/device/verify-authenticated",
+ expect.objectContaining({
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: "user_code=TESTCODE",
+ credentials: "include",
+ }),
+ );
+ });
+ });
+ });
+
+ describe("Authenticated with User Code", () => {
+ it("should show authorization confirmation when authenticated with code in URL", async () => {
+ useIsAuthedMock.mockReturnValue({
+ data: true,
+ isLoading: false,
+ });
+
+ render(
+ ,
+ {
+ wrapper: createWrapper(),
+ },
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("DEVICE$AUTHORIZATION_REQUEST"),
+ ).toBeInTheDocument();
+ });
+
+ expect(screen.getByText("DEVICE$CODE_LABEL")).toBeInTheDocument();
+ expect(screen.getByText("ABC-123")).toBeInTheDocument();
+ expect(screen.getByText("DEVICE$SECURITY_NOTICE")).toBeInTheDocument();
+ expect(screen.getByText("DEVICE$SECURITY_WARNING")).toBeInTheDocument();
+ expect(screen.getByText("DEVICE$CONFIRM_PROMPT")).toBeInTheDocument();
+ });
+
+ it("should show cancel and authorize buttons", async () => {
+ useIsAuthedMock.mockReturnValue({
+ data: true,
+ isLoading: false,
+ });
+
+ render(
+ ,
+ {
+ wrapper: createWrapper(),
+ },
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByRole("button", { name: "DEVICE$CANCEL" }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: "DEVICE$AUTHORIZE" }),
+ ).toBeInTheDocument();
+ });
+ });
+
+ it("should include the EnterpriseBanner component when feature flag is enabled", async () => {
+ useIsAuthedMock.mockReturnValue({
+ data: true,
+ isLoading: false,
+ });
+
+ render(
+ ,
+ {
+ wrapper: createWrapper(),
+ },
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText("ENTERPRISE$TITLE")).toBeInTheDocument();
+ });
+ });
+
+ it("should not include the EnterpriseBanner and be center-aligned when feature flag is disabled", async () => {
+ PROJ_USER_JOURNEY_MOCK.mockReturnValue(false);
+ useIsAuthedMock.mockReturnValue({
+ data: true,
+ isLoading: false,
+ });
+
+ render(
+ ,
+ {
+ wrapper: createWrapper(),
+ },
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("DEVICE$AUTHORIZATION_REQUEST"),
+ ).toBeInTheDocument();
+ });
+
+ // Banner should not be rendered
+ expect(screen.queryByText("ENTERPRISE$TITLE")).not.toBeInTheDocument();
+
+ // Container should use max-w-md (centered layout) instead of max-w-4xl
+ const container = document.querySelector(".max-w-md");
+ expect(container).toBeInTheDocument();
+ expect(document.querySelector(".max-w-4xl")).not.toBeInTheDocument();
+
+ // Authorization card should have mx-auto for centering
+ const authCard = container?.querySelector(".mx-auto");
+ expect(authCard).toBeInTheDocument();
+ });
+
+ it("should call window.close when cancel button is clicked", async () => {
+ const user = userEvent.setup();
+ useIsAuthedMock.mockReturnValue({
+ data: true,
+ isLoading: false,
+ });
+
+ render(
+ ,
+ {
+ wrapper: createWrapper(),
+ },
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByRole("button", { name: "DEVICE$CANCEL" }),
+ ).toBeInTheDocument();
+ });
+
+ const cancelButton = screen.getByRole("button", { name: "DEVICE$CANCEL" });
+ await user.click(cancelButton);
+
+ expect(window.close).toHaveBeenCalled();
+ });
+
+ it("should submit device verification when authorize button is clicked", async () => {
+ const user = userEvent.setup();
+ useIsAuthedMock.mockReturnValue({
+ data: true,
+ isLoading: false,
+ });
+
+ const mockFetch = vi.fn(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({}),
+ }),
+ );
+ vi.stubGlobal("fetch", mockFetch);
+
+ render(
+ ,
+ {
+ wrapper: createWrapper(),
+ },
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByRole("button", { name: "DEVICE$AUTHORIZE" }),
+ ).toBeInTheDocument();
+ });
+
+ const authorizeButton = screen.getByRole("button", {
+ name: "DEVICE$AUTHORIZE",
+ });
+ await user.click(authorizeButton);
+
+ await waitFor(() => {
+ expect(mockFetch).toHaveBeenCalledWith(
+ "/oauth/device/verify-authenticated",
+ expect.objectContaining({
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: "user_code=ABC-123",
+ credentials: "include",
+ }),
+ );
+ });
+ });
+ });
+
+ describe("Processing State", () => {
+ it("should show processing spinner during verification", async () => {
+ const user = userEvent.setup();
+ useIsAuthedMock.mockReturnValue({
+ data: true,
+ isLoading: false,
+ });
+
+ // Make fetch hang to show processing state
+ const mockFetch = vi.fn(() => new Promise(() => {}));
+ vi.stubGlobal("fetch", mockFetch);
+
+ render(
+ ,
+ {
+ wrapper: createWrapper(),
+ },
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByRole("button", { name: "DEVICE$AUTHORIZE" }),
+ ).toBeInTheDocument();
+ });
+
+ const authorizeButton = screen.getByRole("button", {
+ name: "DEVICE$AUTHORIZE",
+ });
+ await user.click(authorizeButton);
+
+ await waitFor(() => {
+ const spinner = document.querySelector(".animate-spin");
+ expect(spinner).toBeInTheDocument();
+ expect(screen.getByText("DEVICE$PROCESSING")).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe("Success State", () => {
+ it("should show success message after successful verification", async () => {
+ const user = userEvent.setup();
+ useIsAuthedMock.mockReturnValue({
+ data: true,
+ isLoading: false,
+ });
+
+ const mockFetch = vi.fn(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({}),
+ }),
+ );
+ vi.stubGlobal("fetch", mockFetch);
+
+ render(
+ ,
+ {
+ wrapper: createWrapper(),
+ },
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByRole("button", { name: "DEVICE$AUTHORIZE" }),
+ ).toBeInTheDocument();
+ });
+
+ const authorizeButton = screen.getByRole("button", {
+ name: "DEVICE$AUTHORIZE",
+ });
+ await user.click(authorizeButton);
+
+ await waitFor(() => {
+ expect(screen.getByText("DEVICE$SUCCESS_TITLE")).toBeInTheDocument();
+ });
+
+ expect(screen.getByText("DEVICE$SUCCESS_MESSAGE")).toBeInTheDocument();
+ // Should show success icon (checkmark)
+ const successIcon = document.querySelector(".text-green-600");
+ expect(successIcon).toBeInTheDocument();
+ });
+
+ it("should not show try again button on success", async () => {
+ const user = userEvent.setup();
+ useIsAuthedMock.mockReturnValue({
+ data: true,
+ isLoading: false,
+ });
+
+ const mockFetch = vi.fn(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({}),
+ }),
+ );
+ vi.stubGlobal("fetch", mockFetch);
+
+ render(
+ ,
+ {
+ wrapper: createWrapper(),
+ },
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByRole("button", { name: "DEVICE$AUTHORIZE" }),
+ ).toBeInTheDocument();
+ });
+
+ const authorizeButton = screen.getByRole("button", {
+ name: "DEVICE$AUTHORIZE",
+ });
+ await user.click(authorizeButton);
+
+ await waitFor(() => {
+ expect(screen.getByText("DEVICE$SUCCESS_TITLE")).toBeInTheDocument();
+ });
+
+ expect(
+ screen.queryByRole("button", { name: "DEVICE$TRY_AGAIN" }),
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ describe("Error State", () => {
+ it("should show error message when verification fails with non-ok response", async () => {
+ const user = userEvent.setup();
+ useIsAuthedMock.mockReturnValue({
+ data: true,
+ isLoading: false,
+ });
+
+ const mockFetch = vi.fn(() =>
+ Promise.resolve({
+ ok: false,
+ status: 400,
+ json: () => Promise.resolve({ error: "invalid_code" }),
+ }),
+ );
+ vi.stubGlobal("fetch", mockFetch);
+
+ render(
+ ,
+ {
+ wrapper: createWrapper(),
+ },
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByRole("button", { name: "DEVICE$AUTHORIZE" }),
+ ).toBeInTheDocument();
+ });
+
+ const authorizeButton = screen.getByRole("button", {
+ name: "DEVICE$AUTHORIZE",
+ });
+ await user.click(authorizeButton);
+
+ await waitFor(() => {
+ expect(screen.getByText("DEVICE$ERROR_TITLE")).toBeInTheDocument();
+ });
+
+ expect(screen.getByText("DEVICE$ERROR_FAILED")).toBeInTheDocument();
+ // Should show error icon (X)
+ const errorIcon = document.querySelector(".text-red-600");
+ expect(errorIcon).toBeInTheDocument();
+ });
+
+ it("should show error message when fetch throws an exception", async () => {
+ const user = userEvent.setup();
+ useIsAuthedMock.mockReturnValue({
+ data: true,
+ isLoading: false,
+ });
+
+ const mockFetch = vi.fn(() => Promise.reject(new Error("Network error")));
+ vi.stubGlobal("fetch", mockFetch);
+
+ render(
+ ,
+ {
+ wrapper: createWrapper(),
+ },
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByRole("button", { name: "DEVICE$AUTHORIZE" }),
+ ).toBeInTheDocument();
+ });
+
+ const authorizeButton = screen.getByRole("button", {
+ name: "DEVICE$AUTHORIZE",
+ });
+ await user.click(authorizeButton);
+
+ await waitFor(() => {
+ expect(screen.getByText("DEVICE$ERROR_TITLE")).toBeInTheDocument();
+ });
+
+ expect(screen.getByText("DEVICE$ERROR_OCCURRED")).toBeInTheDocument();
+ });
+
+ it("should show try again button on error", async () => {
+ const user = userEvent.setup();
+ useIsAuthedMock.mockReturnValue({
+ data: true,
+ isLoading: false,
+ });
+
+ const mockFetch = vi.fn(() =>
+ Promise.resolve({
+ ok: false,
+ status: 400,
+ }),
+ );
+ vi.stubGlobal("fetch", mockFetch);
+
+ render(
+ ,
+ {
+ wrapper: createWrapper(),
+ },
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByRole("button", { name: "DEVICE$AUTHORIZE" }),
+ ).toBeInTheDocument();
+ });
+
+ const authorizeButton = screen.getByRole("button", {
+ name: "DEVICE$AUTHORIZE",
+ });
+ await user.click(authorizeButton);
+
+ await waitFor(() => {
+ expect(
+ screen.getByRole("button", { name: "DEVICE$TRY_AGAIN" }),
+ ).toBeInTheDocument();
+ });
+ });
+
+ it("should reload page when try again button is clicked", async () => {
+ const user = userEvent.setup();
+ useIsAuthedMock.mockReturnValue({
+ data: true,
+ isLoading: false,
+ });
+
+ const mockFetch = vi.fn(() =>
+ Promise.resolve({
+ ok: false,
+ status: 400,
+ }),
+ );
+ vi.stubGlobal("fetch", mockFetch);
+
+ const reloadMock = vi.fn();
+ vi.stubGlobal("location", { reload: reloadMock });
+
+ render(
+ ,
+ {
+ wrapper: createWrapper(),
+ },
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByRole("button", { name: "DEVICE$AUTHORIZE" }),
+ ).toBeInTheDocument();
+ });
+
+ const authorizeButton = screen.getByRole("button", {
+ name: "DEVICE$AUTHORIZE",
+ });
+ await user.click(authorizeButton);
+
+ await waitFor(() => {
+ expect(
+ screen.getByRole("button", { name: "DEVICE$TRY_AGAIN" }),
+ ).toBeInTheDocument();
+ });
+
+ const tryAgainButton = screen.getByRole("button", {
+ name: "DEVICE$TRY_AGAIN",
+ });
+ await user.click(tryAgainButton);
+
+ expect(reloadMock).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/frontend/src/components/features/device-verify/enterprise-banner.tsx b/frontend/src/components/features/device-verify/enterprise-banner.tsx
new file mode 100644
index 0000000000..7746bac48d
--- /dev/null
+++ b/frontend/src/components/features/device-verify/enterprise-banner.tsx
@@ -0,0 +1,69 @@
+import { useTranslation } from "react-i18next";
+import { usePostHog } from "posthog-js/react";
+import { I18nKey } from "#/i18n/declaration";
+import { H2, Text } from "#/ui/typography";
+import CheckCircleFillIcon from "#/icons/check-circle-fill.svg?react";
+import { PROJ_USER_JOURNEY } from "#/utils/feature-flags";
+
+const ENTERPRISE_FEATURE_KEYS: I18nKey[] = [
+ I18nKey.ENTERPRISE$FEATURE_DATA_PRIVACY,
+ I18nKey.ENTERPRISE$FEATURE_DEPLOYMENT,
+ I18nKey.ENTERPRISE$FEATURE_SSO,
+ I18nKey.ENTERPRISE$FEATURE_SUPPORT,
+];
+
+export function EnterpriseBanner() {
+ const { t } = useTranslation();
+ const posthog = usePostHog();
+
+ if (!PROJ_USER_JOURNEY()) {
+ return null;
+ }
+
+ const handleLearnMore = () => {
+ posthog?.capture("saas_selfhosted_inquiry");
+ };
+
+ return (
+
+ {/* Self-Hosted Label */}
+
+
+
+ {t(I18nKey.ENTERPRISE$SELF_HOSTED)}
+
+
+
+
+ {/* Title */}
+
{t(I18nKey.ENTERPRISE$TITLE)}
+
+ {/* Description */}
+
+ {t(I18nKey.ENTERPRISE$DESCRIPTION)}
+
+
+ {/* Features List */}
+
+ {ENTERPRISE_FEATURE_KEYS.map((featureKey) => (
+
+
+ {t(featureKey)}
+
+ ))}
+
+
+ {/* Learn More Button */}
+
+ {t(I18nKey.ENTERPRISE$LEARN_MORE)}
+
+
+ );
+}
diff --git a/frontend/src/i18n/declaration.ts b/frontend/src/i18n/declaration.ts
index 8aef562320..25bb3a7c37 100644
--- a/frontend/src/i18n/declaration.ts
+++ b/frontend/src/i18n/declaration.ts
@@ -1136,4 +1136,34 @@ export enum I18nKey {
ONBOARDING$NEXT_BUTTON = "ONBOARDING$NEXT_BUTTON",
ONBOARDING$BACK_BUTTON = "ONBOARDING$BACK_BUTTON",
ONBOARDING$FINISH_BUTTON = "ONBOARDING$FINISH_BUTTON",
+ ENTERPRISE$SELF_HOSTED = "ENTERPRISE$SELF_HOSTED",
+ ENTERPRISE$TITLE = "ENTERPRISE$TITLE",
+ ENTERPRISE$DESCRIPTION = "ENTERPRISE$DESCRIPTION",
+ ENTERPRISE$FEATURE_DATA_PRIVACY = "ENTERPRISE$FEATURE_DATA_PRIVACY",
+ ENTERPRISE$FEATURE_DEPLOYMENT = "ENTERPRISE$FEATURE_DEPLOYMENT",
+ ENTERPRISE$FEATURE_SSO = "ENTERPRISE$FEATURE_SSO",
+ ENTERPRISE$FEATURE_SUPPORT = "ENTERPRISE$FEATURE_SUPPORT",
+ ENTERPRISE$LEARN_MORE = "ENTERPRISE$LEARN_MORE",
+ ENTERPRISE$LEARN_MORE_ARIA = "ENTERPRISE$LEARN_MORE_ARIA",
+ DEVICE$SUCCESS_TITLE = "DEVICE$SUCCESS_TITLE",
+ DEVICE$ERROR_TITLE = "DEVICE$ERROR_TITLE",
+ DEVICE$SUCCESS_MESSAGE = "DEVICE$SUCCESS_MESSAGE",
+ DEVICE$ERROR_FAILED = "DEVICE$ERROR_FAILED",
+ DEVICE$ERROR_OCCURRED = "DEVICE$ERROR_OCCURRED",
+ DEVICE$TRY_AGAIN = "DEVICE$TRY_AGAIN",
+ DEVICE$PROCESSING = "DEVICE$PROCESSING",
+ DEVICE$AUTHORIZATION_REQUEST = "DEVICE$AUTHORIZATION_REQUEST",
+ DEVICE$CODE_LABEL = "DEVICE$CODE_LABEL",
+ DEVICE$SECURITY_NOTICE = "DEVICE$SECURITY_NOTICE",
+ DEVICE$SECURITY_WARNING = "DEVICE$SECURITY_WARNING",
+ DEVICE$CONFIRM_PROMPT = "DEVICE$CONFIRM_PROMPT",
+ DEVICE$CANCEL = "DEVICE$CANCEL",
+ DEVICE$AUTHORIZE = "DEVICE$AUTHORIZE",
+ DEVICE$AUTHORIZATION_TITLE = "DEVICE$AUTHORIZATION_TITLE",
+ DEVICE$ENTER_CODE_PROMPT = "DEVICE$ENTER_CODE_PROMPT",
+ DEVICE$CODE_INPUT_LABEL = "DEVICE$CODE_INPUT_LABEL",
+ DEVICE$CODE_PLACEHOLDER = "DEVICE$CODE_PLACEHOLDER",
+ DEVICE$CONTINUE = "DEVICE$CONTINUE",
+ DEVICE$AUTH_REQUIRED = "DEVICE$AUTH_REQUIRED",
+ DEVICE$SIGN_IN_PROMPT = "DEVICE$SIGN_IN_PROMPT",
}
diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json
index 5556f891ca..466e9b01cc 100644
--- a/frontend/src/i18n/translation.json
+++ b/frontend/src/i18n/translation.json
@@ -18174,5 +18174,485 @@
"es": "Finalizar",
"tr": "Bitir",
"uk": "Завершити"
+ },
+ "ENTERPRISE$SELF_HOSTED": {
+ "en": "Self-Hosted",
+ "ja": "セルフホスト",
+ "zh-CN": "自托管",
+ "zh-TW": "自託管",
+ "ko-KR": "셀프 호스팅",
+ "no": "Selvhosted",
+ "ar": "مستضاف ذاتياً",
+ "de": "Selbst gehostet",
+ "fr": "Auto-hébergé",
+ "it": "Self-hosted",
+ "pt": "Auto-hospedado",
+ "es": "Autoalojado",
+ "tr": "Kendi Sunucunuzda",
+ "uk": "Самостійний хостинг"
+ },
+ "ENTERPRISE$TITLE": {
+ "en": "OpenHands Enterprise",
+ "ja": "OpenHands Enterprise",
+ "zh-CN": "OpenHands 企业版",
+ "zh-TW": "OpenHands 企業版",
+ "ko-KR": "OpenHands 엔터프라이즈",
+ "no": "OpenHands Enterprise",
+ "ar": "OpenHands للمؤسسات",
+ "de": "OpenHands Enterprise",
+ "fr": "OpenHands Enterprise",
+ "it": "OpenHands Enterprise",
+ "pt": "OpenHands Enterprise",
+ "es": "OpenHands Enterprise",
+ "tr": "OpenHands Kurumsal",
+ "uk": "OpenHands Enterprise"
+ },
+ "ENTERPRISE$DESCRIPTION": {
+ "en": "Complete data control with your own self-hosted AI development platform.",
+ "ja": "独自のセルフホストAI開発プラットフォームで完全なデータ管理を実現。",
+ "zh-CN": "通过自托管AI开发平台实现完全的数据控制。",
+ "zh-TW": "透過自託管AI開發平台實現完全的資料控制。",
+ "ko-KR": "셀프 호스팅 AI 개발 플랫폼으로 완벽한 데이터 제어를 실현하세요.",
+ "no": "Fullstendig datakontroll med din egen selvhostede AI-utviklingsplattform.",
+ "ar": "تحكم كامل في البيانات مع منصة تطوير الذكاء الاصطناعي المستضافة ذاتياً.",
+ "de": "Vollständige Datenkontrolle mit Ihrer eigenen selbst gehosteten KI-Entwicklungsplattform.",
+ "fr": "Contrôle total des données avec votre propre plateforme de développement IA auto-hébergée.",
+ "it": "Controllo completo dei dati con la tua piattaforma di sviluppo AI self-hosted.",
+ "pt": "Controle completo de dados com sua própria plataforma de desenvolvimento de IA auto-hospedada.",
+ "es": "Control completo de datos con tu propia plataforma de desarrollo de IA autoalojada.",
+ "tr": "Kendi barındırdığınız yapay zeka geliştirme platformuyla tam veri kontrolü.",
+ "uk": "Повний контроль над даними з власною самостійно розміщеною платформою розробки ШІ."
+ },
+ "ENTERPRISE$FEATURE_DATA_PRIVACY": {
+ "en": "Full data privacy & control",
+ "ja": "完全なデータプライバシーと管理",
+ "zh-CN": "完全的数据隐私和控制",
+ "zh-TW": "完全的資料隱私和控制",
+ "ko-KR": "완벽한 데이터 프라이버시 및 제어",
+ "no": "Full datapersonvern og kontroll",
+ "ar": "خصوصية وتحكم كامل في البيانات",
+ "de": "Vollständiger Datenschutz und Kontrolle",
+ "fr": "Confidentialité et contrôle complets des données",
+ "it": "Privacy e controllo completo dei dati",
+ "pt": "Privacidade e controle total de dados",
+ "es": "Privacidad y control total de datos",
+ "tr": "Tam veri gizliliği ve kontrolü",
+ "uk": "Повна конфіденційність та контроль даних"
+ },
+ "ENTERPRISE$FEATURE_DEPLOYMENT": {
+ "en": "Custom deployment options",
+ "ja": "カスタムデプロイオプション",
+ "zh-CN": "自定义部署选项",
+ "zh-TW": "自訂部署選項",
+ "ko-KR": "맞춤형 배포 옵션",
+ "no": "Tilpassede distribusjonsalternativer",
+ "ar": "خيارات نشر مخصصة",
+ "de": "Individuelle Bereitstellungsoptionen",
+ "fr": "Options de déploiement personnalisées",
+ "it": "Opzioni di distribuzione personalizzate",
+ "pt": "Opções de implantação personalizadas",
+ "es": "Opciones de despliegue personalizadas",
+ "tr": "Özel dağıtım seçenekleri",
+ "uk": "Налаштовані варіанти розгортання"
+ },
+ "ENTERPRISE$FEATURE_SSO": {
+ "en": "SSO & enterprise auth",
+ "ja": "SSOとエンタープライズ認証",
+ "zh-CN": "SSO和企业认证",
+ "zh-TW": "SSO和企業認證",
+ "ko-KR": "SSO 및 엔터프라이즈 인증",
+ "no": "SSO og bedriftsautentisering",
+ "ar": "تسجيل دخول موحد ومصادقة المؤسسات",
+ "de": "SSO und Unternehmensauthentifizierung",
+ "fr": "SSO et authentification d'entreprise",
+ "it": "SSO e autenticazione aziendale",
+ "pt": "SSO e autenticação empresarial",
+ "es": "SSO y autenticación empresarial",
+ "tr": "SSO ve kurumsal kimlik doğrulama",
+ "uk": "SSO та корпоративна автентифікація"
+ },
+ "ENTERPRISE$FEATURE_SUPPORT": {
+ "en": "Dedicated support",
+ "ja": "専用サポート",
+ "zh-CN": "专属支持",
+ "zh-TW": "專屬支援",
+ "ko-KR": "전담 지원",
+ "no": "Dedikert støtte",
+ "ar": "دعم مخصص",
+ "de": "Dedizierter Support",
+ "fr": "Support dédié",
+ "it": "Supporto dedicato",
+ "pt": "Suporte dedicado",
+ "es": "Soporte dedicado",
+ "tr": "Özel destek",
+ "uk": "Виділена підтримка"
+ },
+ "ENTERPRISE$LEARN_MORE": {
+ "en": "Learn More",
+ "ja": "詳細を見る",
+ "zh-CN": "了解更多",
+ "zh-TW": "了解更多",
+ "ko-KR": "더 알아보기",
+ "no": "Les mer",
+ "ar": "اعرف المزيد",
+ "de": "Mehr erfahren",
+ "fr": "En savoir plus",
+ "it": "Scopri di più",
+ "pt": "Saiba mais",
+ "es": "Más información",
+ "tr": "Daha Fazla Bilgi",
+ "uk": "Дізнатися більше"
+ },
+ "ENTERPRISE$LEARN_MORE_ARIA": {
+ "en": "Learn more about OpenHands Enterprise (opens in new window)",
+ "ja": "OpenHands Enterpriseの詳細を見る(新しいウィンドウで開く)",
+ "zh-CN": "了解更多关于 OpenHands 企业版的信息(在新窗口中打开)",
+ "zh-TW": "了解更多關於 OpenHands 企業版的資訊(在新視窗中開啟)",
+ "ko-KR": "OpenHands 엔터프라이즈에 대해 더 알아보기 (새 창에서 열림)",
+ "no": "Les mer om OpenHands Enterprise (åpnes i nytt vindu)",
+ "ar": "اعرف المزيد عن OpenHands Enterprise (يفتح في نافذة جديدة)",
+ "de": "Erfahren Sie mehr über OpenHands Enterprise (öffnet in neuem Fenster)",
+ "fr": "En savoir plus sur OpenHands Enterprise (s'ouvre dans une nouvelle fenêtre)",
+ "it": "Scopri di più su OpenHands Enterprise (si apre in una nuova finestra)",
+ "pt": "Saiba mais sobre OpenHands Enterprise (abre em nova janela)",
+ "es": "Más información sobre OpenHands Enterprise (abre en nueva ventana)",
+ "tr": "OpenHands Enterprise hakkında daha fazla bilgi edinin (yeni pencerede açılır)",
+ "uk": "Дізнатися більше про OpenHands Enterprise (відкривається в новому вікні)"
+ },
+ "DEVICE$SUCCESS_TITLE": {
+ "en": "Success!",
+ "ja": "成功!",
+ "zh-CN": "成功!",
+ "zh-TW": "成功!",
+ "ko-KR": "성공!",
+ "no": "Suksess!",
+ "ar": "نجاح!",
+ "de": "Erfolg!",
+ "fr": "Succès !",
+ "it": "Successo!",
+ "pt": "Sucesso!",
+ "es": "¡Éxito!",
+ "tr": "Başarılı!",
+ "uk": "Успіх!"
+ },
+ "DEVICE$ERROR_TITLE": {
+ "en": "Error",
+ "ja": "エラー",
+ "zh-CN": "错误",
+ "zh-TW": "錯誤",
+ "ko-KR": "오류",
+ "no": "Feil",
+ "ar": "خطأ",
+ "de": "Fehler",
+ "fr": "Erreur",
+ "it": "Errore",
+ "pt": "Erro",
+ "es": "Error",
+ "tr": "Hata",
+ "uk": "Помилка"
+ },
+ "DEVICE$SUCCESS_MESSAGE": {
+ "en": "Device authorized successfully! You can now return to your CLI and close this window.",
+ "ja": "デバイスが正常に認証されました!CLIに戻り、このウィンドウを閉じてください。",
+ "zh-CN": "设备授权成功!您现在可以返回CLI并关闭此窗口。",
+ "zh-TW": "設備授權成功!您現在可以返回 CLI 並關閉此視窗。",
+ "ko-KR": "기기가 성공적으로 인증되었습니다! CLI로 돌아가서 이 창을 닫으세요.",
+ "no": "Enheten er autorisert! Du kan nå gå tilbake til CLI og lukke dette vinduet.",
+ "ar": "تم ترخيص الجهاز بنجاح! يمكنك الآن العودة إلى CLI وإغلاق هذه النافذة.",
+ "de": "Gerät erfolgreich autorisiert! Sie können jetzt zu Ihrer CLI zurückkehren und dieses Fenster schließen.",
+ "fr": "Appareil autorisé avec succès ! Vous pouvez maintenant retourner à votre CLI et fermer cette fenêtre.",
+ "it": "Dispositivo autorizzato con successo! Ora puoi tornare alla CLI e chiudere questa finestra.",
+ "pt": "Dispositivo autorizado com sucesso! Você pode voltar ao CLI e fechar esta janela.",
+ "es": "¡Dispositivo autorizado exitosamente! Ahora puedes volver a tu CLI y cerrar esta ventana.",
+ "tr": "Cihaz başarıyla yetkilendirildi! Artık CLI'nize dönebilir ve bu pencereyi kapatabilirsiniz.",
+ "uk": "Пристрій успішно авторизовано! Тепер ви можете повернутися до CLI та закрити це вікно."
+ },
+ "DEVICE$ERROR_FAILED": {
+ "en": "Failed to authorize device. Please try again.",
+ "ja": "デバイスの認証に失敗しました。もう一度お試しください。",
+ "zh-CN": "设备授权失败。请重试。",
+ "zh-TW": "設備授權失敗。請重試。",
+ "ko-KR": "기기 인증에 실패했습니다. 다시 시도해 주세요.",
+ "no": "Kunne ikke autorisere enheten. Vennligst prøv igjen.",
+ "ar": "فشل في ترخيص الجهاز. يرجى المحاولة مرة أخرى.",
+ "de": "Geräteautorisierung fehlgeschlagen. Bitte versuchen Sie es erneut.",
+ "fr": "Échec de l'autorisation de l'appareil. Veuillez réessayer.",
+ "it": "Autorizzazione dispositivo fallita. Riprova.",
+ "pt": "Falha ao autorizar o dispositivo. Por favor, tente novamente.",
+ "es": "Error al autorizar el dispositivo. Por favor, inténtalo de nuevo.",
+ "tr": "Cihaz yetkilendirilemedi. Lütfen tekrar deneyin.",
+ "uk": "Не вдалося авторизувати пристрій. Будь ласка, спробуйте ще раз."
+ },
+ "DEVICE$ERROR_OCCURRED": {
+ "en": "An error occurred while authorizing the device. Please try again.",
+ "ja": "デバイスの認証中にエラーが発生しました。もう一度お試しください。",
+ "zh-CN": "授权设备时发生错误。请重试。",
+ "zh-TW": "授權設備時發生錯誤。請重試。",
+ "ko-KR": "기기 인증 중 오류가 발생했습니다. 다시 시도해 주세요.",
+ "no": "En feil oppstod under autorisering av enheten. Vennligst prøv igjen.",
+ "ar": "حدث خطأ أثناء ترخيص الجهاز. يرجى المحاولة مرة أخرى.",
+ "de": "Bei der Geräteautorisierung ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.",
+ "fr": "Une erreur s'est produite lors de l'autorisation de l'appareil. Veuillez réessayer.",
+ "it": "Si è verificato un errore durante l'autorizzazione del dispositivo. Riprova.",
+ "pt": "Ocorreu um erro ao autorizar o dispositivo. Por favor, tente novamente.",
+ "es": "Ocurrió un error al autorizar el dispositivo. Por favor, inténtalo de nuevo.",
+ "tr": "Cihaz yetkilendirirken bir hata oluştu. Lütfen tekrar deneyin.",
+ "uk": "Під час авторизації пристрою сталася помилка. Будь ласка, спробуйте ще раз."
+ },
+ "DEVICE$TRY_AGAIN": {
+ "en": "Try Again",
+ "ja": "再試行",
+ "zh-CN": "重试",
+ "zh-TW": "重試",
+ "ko-KR": "다시 시도",
+ "no": "Prøv igjen",
+ "ar": "حاول مرة أخرى",
+ "de": "Erneut versuchen",
+ "fr": "Réessayer",
+ "it": "Riprova",
+ "pt": "Tentar novamente",
+ "es": "Intentar de nuevo",
+ "tr": "Tekrar Dene",
+ "uk": "Спробувати ще раз"
+ },
+ "DEVICE$PROCESSING": {
+ "en": "Processing device verification...",
+ "ja": "デバイス認証を処理中...",
+ "zh-CN": "正在处理设备验证...",
+ "zh-TW": "正在處理設備驗證...",
+ "ko-KR": "기기 인증 처리 중...",
+ "no": "Behandler enhetsverifisering...",
+ "ar": "جارٍ معالجة التحقق من الجهاز...",
+ "de": "Geräteverifizierung wird verarbeitet...",
+ "fr": "Traitement de la vérification de l'appareil...",
+ "it": "Elaborazione verifica dispositivo...",
+ "pt": "Processando verificação do dispositivo...",
+ "es": "Procesando verificación del dispositivo...",
+ "tr": "Cihaz doğrulaması işleniyor...",
+ "uk": "Обробка перевірки пристрою..."
+ },
+ "DEVICE$AUTHORIZATION_REQUEST": {
+ "en": "Device Authorization Request",
+ "ja": "デバイス認証リクエスト",
+ "zh-CN": "设备授权请求",
+ "zh-TW": "設備授權請求",
+ "ko-KR": "기기 인증 요청",
+ "no": "Forespørsel om enhetsautorisasjon",
+ "ar": "طلب ترخيص الجهاز",
+ "de": "Geräteautorisierungsanfrage",
+ "fr": "Demande d'autorisation d'appareil",
+ "it": "Richiesta di autorizzazione dispositivo",
+ "pt": "Solicitação de autorização do dispositivo",
+ "es": "Solicitud de autorización del dispositivo",
+ "tr": "Cihaz Yetkilendirme Talebi",
+ "uk": "Запит на авторизацію пристрою"
+ },
+ "DEVICE$CODE_LABEL": {
+ "en": "DEVICE CODE",
+ "ja": "デバイスコード",
+ "zh-CN": "设备代码",
+ "zh-TW": "設備代碼",
+ "ko-KR": "기기 코드",
+ "no": "ENHETSKODE",
+ "ar": "رمز الجهاز",
+ "de": "GERÄTECODE",
+ "fr": "CODE DE L'APPAREIL",
+ "it": "CODICE DISPOSITIVO",
+ "pt": "CÓDIGO DO DISPOSITIVO",
+ "es": "CÓDIGO DE DISPOSITIVO",
+ "tr": "CİHAZ KODU",
+ "uk": "КОД ПРИСТРОЮ"
+ },
+ "DEVICE$SECURITY_NOTICE": {
+ "en": "Security Notice",
+ "ja": "セキュリティ通知",
+ "zh-CN": "安全提示",
+ "zh-TW": "安全提示",
+ "ko-KR": "보안 알림",
+ "no": "Sikkerhetsvarsel",
+ "ar": "إشعار أمني",
+ "de": "Sicherheitshinweis",
+ "fr": "Avis de sécurité",
+ "it": "Avviso di sicurezza",
+ "pt": "Aviso de segurança",
+ "es": "Aviso de seguridad",
+ "tr": "Güvenlik Bildirimi",
+ "uk": "Повідомлення про безпеку"
+ },
+ "DEVICE$SECURITY_WARNING": {
+ "en": "Only authorize this device if you initiated this request from your CLI or application.",
+ "ja": "CLIまたはアプリケーションからこのリクエストを開始した場合のみ、このデバイスを認証してください。",
+ "zh-CN": "仅当您从 CLI 或应用程序发起此请求时,才授权此设备。",
+ "zh-TW": "僅當您從 CLI 或應用程式發起此請求時,才授權此設備。",
+ "ko-KR": "CLI 또는 애플리케이션에서 이 요청을 시작한 경우에만 이 기기를 인증하세요.",
+ "no": "Bare autoriser denne enheten hvis du startet denne forespørselen fra CLI eller applikasjonen din.",
+ "ar": "قم بترخيص هذا الجهاز فقط إذا كنت قد بدأت هذا الطلب من CLI أو التطبيق الخاص بك.",
+ "de": "Autorisieren Sie dieses Gerät nur, wenn Sie diese Anfrage von Ihrer CLI oder Anwendung aus gestartet haben.",
+ "fr": "N'autorisez cet appareil que si vous avez initié cette demande depuis votre CLI ou application.",
+ "it": "Autorizza questo dispositivo solo se hai avviato questa richiesta dalla tua CLI o applicazione.",
+ "pt": "Autorize este dispositivo apenas se você iniciou esta solicitação do seu CLI ou aplicativo.",
+ "es": "Solo autoriza este dispositivo si iniciaste esta solicitud desde tu CLI o aplicación.",
+ "tr": "Bu cihazı yalnızca bu isteği CLI veya uygulamanızdan başlattıysanız yetkilendirin.",
+ "uk": "Авторизуйте цей пристрій лише якщо ви ініціювали цей запит з вашого CLI або додатку."
+ },
+ "DEVICE$CONFIRM_PROMPT": {
+ "en": "Do you want to authorize this device to access your OpenHands account?",
+ "ja": "このデバイスにOpenHandsアカウントへのアクセスを許可しますか?",
+ "zh-CN": "您想授权此设备访问您的 OpenHands 帐户吗?",
+ "zh-TW": "您想授權此設備訪問您的 OpenHands 帳戶嗎?",
+ "ko-KR": "이 기기가 OpenHands 계정에 액세스하도록 인증하시겠습니까?",
+ "no": "Vil du autorisere denne enheten til å få tilgang til din OpenHands-konto?",
+ "ar": "هل تريد ترخيص هذا الجهاز للوصول إلى حسابك في OpenHands؟",
+ "de": "Möchten Sie dieses Gerät autorisieren, um auf Ihr OpenHands-Konto zuzugreifen?",
+ "fr": "Voulez-vous autoriser cet appareil à accéder à votre compte OpenHands ?",
+ "it": "Vuoi autorizzare questo dispositivo ad accedere al tuo account OpenHands?",
+ "pt": "Deseja autorizar este dispositivo a acessar sua conta OpenHands?",
+ "es": "¿Deseas autorizar este dispositivo para acceder a tu cuenta de OpenHands?",
+ "tr": "Bu cihazın OpenHands hesabınıza erişmesine izin vermek istiyor musunuz?",
+ "uk": "Бажаєте авторизувати цей пристрій для доступу до вашого облікового запису OpenHands?"
+ },
+ "DEVICE$CANCEL": {
+ "en": "Cancel",
+ "ja": "キャンセル",
+ "zh-CN": "取消",
+ "zh-TW": "取消",
+ "ko-KR": "취소",
+ "no": "Avbryt",
+ "ar": "إلغاء",
+ "de": "Abbrechen",
+ "fr": "Annuler",
+ "it": "Annulla",
+ "pt": "Cancelar",
+ "es": "Cancelar",
+ "tr": "İptal",
+ "uk": "Скасувати"
+ },
+ "DEVICE$AUTHORIZE": {
+ "en": "Authorize Device",
+ "ja": "デバイスを認証",
+ "zh-CN": "授权设备",
+ "zh-TW": "授權設備",
+ "ko-KR": "기기 인증",
+ "no": "Autoriser enhet",
+ "ar": "ترخيص الجهاز",
+ "de": "Gerät autorisieren",
+ "fr": "Autoriser l'appareil",
+ "it": "Autorizza dispositivo",
+ "pt": "Autorizar dispositivo",
+ "es": "Autorizar dispositivo",
+ "tr": "Cihazı Yetkilendir",
+ "uk": "Авторизувати пристрій"
+ },
+ "DEVICE$AUTHORIZATION_TITLE": {
+ "en": "Device Authorization",
+ "ja": "デバイス認証",
+ "zh-CN": "设备授权",
+ "zh-TW": "設備授權",
+ "ko-KR": "기기 인증",
+ "no": "Enhetsautorisasjon",
+ "ar": "ترخيص الجهاز",
+ "de": "Geräteautorisierung",
+ "fr": "Autorisation d'appareil",
+ "it": "Autorizzazione dispositivo",
+ "pt": "Autorização do dispositivo",
+ "es": "Autorización del dispositivo",
+ "tr": "Cihaz Yetkilendirme",
+ "uk": "Авторизація пристрою"
+ },
+ "DEVICE$ENTER_CODE_PROMPT": {
+ "en": "Enter the code displayed on your device:",
+ "ja": "デバイスに表示されているコードを入力してください:",
+ "zh-CN": "输入设备上显示的代码:",
+ "zh-TW": "輸入設備上顯示的代碼:",
+ "ko-KR": "기기에 표시된 코드를 입력하세요:",
+ "no": "Skriv inn koden som vises på enheten din:",
+ "ar": "أدخل الرمز المعروض على جهازك:",
+ "de": "Geben Sie den auf Ihrem Gerät angezeigten Code ein:",
+ "fr": "Entrez le code affiché sur votre appareil :",
+ "it": "Inserisci il codice visualizzato sul tuo dispositivo:",
+ "pt": "Digite o código exibido no seu dispositivo:",
+ "es": "Ingresa el código mostrado en tu dispositivo:",
+ "tr": "Cihazınızda görüntülenen kodu girin:",
+ "uk": "Введіть код, відображений на вашому пристрої:"
+ },
+ "DEVICE$CODE_INPUT_LABEL": {
+ "en": "Device Code:",
+ "ja": "デバイスコード:",
+ "zh-CN": "设备代码:",
+ "zh-TW": "設備代碼:",
+ "ko-KR": "기기 코드:",
+ "no": "Enhetskode:",
+ "ar": "رمز الجهاز:",
+ "de": "Gerätecode:",
+ "fr": "Code de l'appareil :",
+ "it": "Codice dispositivo:",
+ "pt": "Código do dispositivo:",
+ "es": "Código del dispositivo:",
+ "tr": "Cihaz Kodu:",
+ "uk": "Код пристрою:"
+ },
+ "DEVICE$CODE_PLACEHOLDER": {
+ "en": "Enter your device code",
+ "ja": "デバイスコードを入力",
+ "zh-CN": "输入您的设备代码",
+ "zh-TW": "輸入您的設備代碼",
+ "ko-KR": "기기 코드를 입력하세요",
+ "no": "Skriv inn enhetskoden din",
+ "ar": "أدخل رمز جهازك",
+ "de": "Geben Sie Ihren Gerätecode ein",
+ "fr": "Entrez votre code d'appareil",
+ "it": "Inserisci il tuo codice dispositivo",
+ "pt": "Digite o código do seu dispositivo",
+ "es": "Ingresa tu código de dispositivo",
+ "tr": "Cihaz kodunuzu girin",
+ "uk": "Введіть код вашого пристрою"
+ },
+ "DEVICE$CONTINUE": {
+ "en": "Continue",
+ "ja": "続行",
+ "zh-CN": "继续",
+ "zh-TW": "繼續",
+ "ko-KR": "계속",
+ "no": "Fortsett",
+ "ar": "متابعة",
+ "de": "Fortfahren",
+ "fr": "Continuer",
+ "it": "Continua",
+ "pt": "Continuar",
+ "es": "Continuar",
+ "tr": "Devam",
+ "uk": "Продовжити"
+ },
+ "DEVICE$AUTH_REQUIRED": {
+ "en": "Authentication Required",
+ "ja": "認証が必要です",
+ "zh-CN": "需要身份验证",
+ "zh-TW": "需要身份驗證",
+ "ko-KR": "인증 필요",
+ "no": "Autentisering kreves",
+ "ar": "المصادقة مطلوبة",
+ "de": "Authentifizierung erforderlich",
+ "fr": "Authentification requise",
+ "it": "Autenticazione richiesta",
+ "pt": "Autenticação necessária",
+ "es": "Autenticación requerida",
+ "tr": "Kimlik Doğrulama Gerekli",
+ "uk": "Потрібна автентифікація"
+ },
+ "DEVICE$SIGN_IN_PROMPT": {
+ "en": "Please sign in to authorize your device.",
+ "ja": "デバイスを認証するにはサインインしてください。",
+ "zh-CN": "请登录以授权您的设备。",
+ "zh-TW": "請登入以授權您的設備。",
+ "ko-KR": "기기를 인증하려면 로그인하세요.",
+ "no": "Vennligst logg inn for å autorisere enheten din.",
+ "ar": "يرجى تسجيل الدخول لترخيص جهازك.",
+ "de": "Bitte melden Sie sich an, um Ihr Gerät zu autorisieren.",
+ "fr": "Veuillez vous connecter pour autoriser votre appareil.",
+ "it": "Accedi per autorizzare il tuo dispositivo.",
+ "pt": "Por favor, faça login para autorizar seu dispositivo.",
+ "es": "Por favor, inicia sesión para autorizar tu dispositivo.",
+ "tr": "Cihazınızı yetkilendirmek için lütfen giriş yapın.",
+ "uk": "Будь ласка, увійдіть, щоб авторизувати свій пристрій."
}
}
diff --git a/frontend/src/icons/check-circle-fill.svg b/frontend/src/icons/check-circle-fill.svg
new file mode 100644
index 0000000000..c13fa064a5
--- /dev/null
+++ b/frontend/src/icons/check-circle-fill.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/src/routes/device-verify.tsx b/frontend/src/routes/device-verify.tsx
index f306d660a5..aabc94e544 100644
--- a/frontend/src/routes/device-verify.tsx
+++ b/frontend/src/routes/device-verify.tsx
@@ -1,16 +1,22 @@
-/* eslint-disable i18next/no-literal-string */
import React, { useState } from "react";
import { useSearchParams } from "react-router";
+import { useTranslation } from "react-i18next";
import { useIsAuthed } from "#/hooks/query/use-is-authed";
+import { EnterpriseBanner } from "#/components/features/device-verify/enterprise-banner";
+import { I18nKey } from "#/i18n/declaration";
+import { H1 } from "#/ui/typography";
+import { PROJ_USER_JOURNEY } from "#/utils/feature-flags";
export default function DeviceVerify() {
+ const { t } = useTranslation();
const [searchParams] = useSearchParams();
const { data: isAuthed, isLoading: isAuthLoading } = useIsAuthed();
const [verificationResult, setVerificationResult] = useState<{
success: boolean;
- message: string;
+ messageKey: I18nKey;
} | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
+ const showEnterpriseBanner = PROJ_USER_JOURNEY();
// Get user_code from URL parameters
const userCode = searchParams.get("user_code");
@@ -33,21 +39,18 @@ export default function DeviceVerify() {
// Show success message
setVerificationResult({
success: true,
- message:
- "Device authorized successfully! You can now return to your CLI and close this window.",
+ messageKey: I18nKey.DEVICE$SUCCESS_MESSAGE,
});
} else {
- const errorText = await response.text();
setVerificationResult({
success: false,
- message: errorText || "Failed to authorize device. Please try again.",
+ messageKey: I18nKey.DEVICE$ERROR_FAILED,
});
}
} catch (error) {
setVerificationResult({
success: false,
- message:
- "An error occurred while authorizing the device. Please try again.",
+ messageKey: I18nKey.DEVICE$ERROR_OCCURRED,
});
} finally {
setIsProcessing(false);
@@ -105,10 +108,12 @@ export default function DeviceVerify() {
)}
- {verificationResult.success ? "Success!" : "Error"}
+ {verificationResult.success
+ ? t(I18nKey.DEVICE$SUCCESS_TITLE)
+ : t(I18nKey.DEVICE$ERROR_TITLE)}
- {verificationResult.message}
+ {t(verificationResult.messageKey)}
{!verificationResult.success && (
window.location.reload()}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
>
- Try Again
+ {t(I18nKey.DEVICE$TRY_AGAIN)}
)}
@@ -133,7 +138,7 @@ export default function DeviceVerify() {
- Processing device verification...
+ {t(I18nKey.DEVICE$PROCESSING)}
@@ -144,63 +149,56 @@ export default function DeviceVerify() {
// Show device authorization confirmation if user is authenticated and code is provided
if (isAuthed && userCode) {
return (
-
-
-
- Device Authorization Request
-
-
-
Device Code:
-
- {userCode}
+
+
+ {/* Device Authorization Card */}
+
+
+ {t(I18nKey.DEVICE$AUTHORIZATION_REQUEST)}
+
+
+
+ {t(I18nKey.DEVICE$CODE_LABEL)}
+
+
+ {userCode}
+
+
+
+
+ {t(I18nKey.DEVICE$SECURITY_NOTICE)}
+
+
+ {t(I18nKey.DEVICE$SECURITY_WARNING)}
+
+
+
+ {t(I18nKey.DEVICE$CONFIRM_PROMPT)}
-
-
-
-
+ window.close()}
+ className="flex-1 px-4 py-2 border border-neutral-600 rounded-md hover:bg-muted text-gray-300"
>
-
-
-
-
- Security Notice
-
-
- Only authorize this device if you initiated this request from
- your CLI or application.
-
-
+ {t(I18nKey.DEVICE$CANCEL)}
+
+
processDeviceVerification(userCode)}
+ className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
+ >
+ {t(I18nKey.DEVICE$AUTHORIZE)}
+
-
- Do you want to authorize this device to access your OpenHands
- account?
-
-
- window.close()}
- className="flex-1 px-4 py-2 border border-input rounded-md hover:bg-muted"
- >
- Cancel
-
- processDeviceVerification(userCode)}
- className="flex-1 px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
- >
- Authorize Device
-
-
+
+ {/* Enterprise Banner */}
+ {showEnterpriseBanner &&
}
);
@@ -211,11 +209,11 @@ export default function DeviceVerify() {
return (
-
- Device Authorization
-
+
+ {t(I18nKey.DEVICE$AUTHORIZATION_TITLE)}
+
- Enter the code displayed on your device:
+ {t(I18nKey.DEVICE$ENTER_CODE_PROMPT)}
@@ -253,7 +251,7 @@ export default function DeviceVerify() {
- Processing device verification...
+ {t(I18nKey.DEVICE$PROCESSING)}
@@ -264,9 +262,9 @@ export default function DeviceVerify() {
return (
-
Authentication Required
+
{t(I18nKey.DEVICE$AUTH_REQUIRED)}
- Please sign in to authorize your device.
+ {t(I18nKey.DEVICE$SIGN_IN_PROMPT)}
diff --git a/frontend/src/utils/feature-flags.ts b/frontend/src/utils/feature-flags.ts
index fd5495d33b..ba691e76b7 100644
--- a/frontend/src/utils/feature-flags.ts
+++ b/frontend/src/utils/feature-flags.ts
@@ -20,3 +20,4 @@ export const ENABLE_TRAJECTORY_REPLAY = () =>
export const ENABLE_ONBOARDING = () => loadFeatureFlag("ENABLE_ONBOARDING");
export const ENABLE_SANDBOX_GROUPING = () =>
loadFeatureFlag("SANDBOX_GROUPING");
+export const PROJ_USER_JOURNEY = () => loadFeatureFlag("PROJ_USER_JOURNEY");