From 934fbe93c262605d3b0613d62beaeccbcab20008 Mon Sep 17 00:00:00 2001 From: Rohit Malhotra Date: Mon, 16 Mar 2026 14:54:36 -0400 Subject: [PATCH] Feat: enterprise banner option during device oauth (#13361) Co-authored-by: openhands --- .../device-verify/enterprise-banner.test.tsx | 118 ++++ .../__tests__/routes/device-verify.test.tsx | 659 ++++++++++++++++++ .../device-verify/enterprise-banner.tsx | 69 ++ frontend/src/i18n/declaration.ts | 30 + frontend/src/i18n/translation.json | 480 +++++++++++++ frontend/src/icons/check-circle-fill.svg | 3 + frontend/src/routes/device-verify.tsx | 148 ++-- frontend/src/utils/feature-flags.ts | 1 + 8 files changed, 1433 insertions(+), 75 deletions(-) create mode 100644 frontend/__tests__/components/features/device-verify/enterprise-banner.test.tsx create mode 100644 frontend/__tests__/routes/device-verify.test.tsx create mode 100644 frontend/src/components/features/device-verify/enterprise-banner.tsx create mode 100644 frontend/src/icons/check-circle-fill.svg 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 && ( )} @@ -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)}

-
-
-
- + -
-

- Security Notice -

-

- Only authorize this device if you initiated this request from - your CLI or application. -

-
+ {t(I18nKey.DEVICE$CANCEL)} + +
-

- Do you want to authorize this device to access your OpenHands - account? -

-
- - -
+ + {/* 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)}

@@ -223,7 +221,7 @@ export default function DeviceVerify() { htmlFor="user_code" className="block text-sm font-medium mb-2" > - Device Code: + {t(I18nKey.DEVICE$CODE_INPUT_LABEL)}
@@ -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");