Feat: enterprise banner option during device oauth (#13361)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Rohit Malhotra
2026-03-16 14:54:36 -04:00
committed by GitHub
parent 55e4f07200
commit 934fbe93c2
8 changed files with 1433 additions and 75 deletions

View File

@@ -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(<EnterpriseBanner />);
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(<EnterpriseBanner />);
expect(screen.getByText("ENTERPRISE$TITLE")).toBeInTheDocument();
});
});
describe("Rendering", () => {
it("should render the self-hosted label", () => {
renderWithProviders(<EnterpriseBanner />);
expect(screen.getByText("ENTERPRISE$SELF_HOSTED")).toBeInTheDocument();
});
it("should render the enterprise title", () => {
renderWithProviders(<EnterpriseBanner />);
expect(screen.getByText("ENTERPRISE$TITLE")).toBeInTheDocument();
});
it("should render the enterprise description", () => {
renderWithProviders(<EnterpriseBanner />);
expect(screen.getByText("ENTERPRISE$DESCRIPTION")).toBeInTheDocument();
});
it("should render all four enterprise feature items", () => {
renderWithProviders(<EnterpriseBanner />);
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(<EnterpriseBanner />);
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(<EnterpriseBanner />);
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(<EnterpriseBanner />);
const link = screen.getByRole("link", {
name: "ENTERPRISE$LEARN_MORE_ARIA",
});
expect(link).toHaveAttribute("href", "https://openhands.dev/enterprise");
expect(link).toHaveAttribute("target", "_blank");
});
});
});

View File

@@ -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 (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
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(<RouterStub initialEntries={["/device-verify"]} />, {
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(<RouterStub initialEntries={["/device-verify"]} />, {
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(<RouterStub initialEntries={["/device-verify"]} />, {
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(<RouterStub initialEntries={["/device-verify"]} />, {
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(
<RouterStub initialEntries={["/device-verify?user_code=ABC-123"]} />,
{
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(
<RouterStub initialEntries={["/device-verify?user_code=ABC-123"]} />,
{
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(
<RouterStub initialEntries={["/device-verify?user_code=ABC-123"]} />,
{
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(
<RouterStub initialEntries={["/device-verify?user_code=ABC-123"]} />,
{
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(
<RouterStub initialEntries={["/device-verify?user_code=ABC-123"]} />,
{
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(
<RouterStub initialEntries={["/device-verify?user_code=ABC-123"]} />,
{
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(
<RouterStub initialEntries={["/device-verify?user_code=ABC-123"]} />,
{
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(
<RouterStub initialEntries={["/device-verify?user_code=ABC-123"]} />,
{
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(
<RouterStub initialEntries={["/device-verify?user_code=ABC-123"]} />,
{
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(
<RouterStub initialEntries={["/device-verify?user_code=INVALID"]} />,
{
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(
<RouterStub initialEntries={["/device-verify?user_code=ABC-123"]} />,
{
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(
<RouterStub initialEntries={["/device-verify?user_code=INVALID"]} />,
{
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(
<RouterStub initialEntries={["/device-verify?user_code=INVALID"]} />,
{
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();
});
});
});

View File

@@ -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 (
<div className="w-full max-w-md mx-auto lg:mx-0 lg:w-80 p-6 rounded-lg bg-gradient-to-b from-slate-800 to-slate-900 border border-slate-700 h-fit">
{/* Self-Hosted Label */}
<div className="flex justify-center mb-4">
<div className="px-8 py-0.5 rounded-full bg-gradient-to-r from-blue-900 to-blue-950 border border-blue-800">
<Text className="text-xs font-medium text-blue-400 tracking-wider uppercase">
{t(I18nKey.ENTERPRISE$SELF_HOSTED)}
</Text>
</div>
</div>
{/* Title */}
<H2 className="text-center mb-3">{t(I18nKey.ENTERPRISE$TITLE)}</H2>
{/* Description */}
<Text className="text-sm text-gray-400 text-center mb-6 block">
{t(I18nKey.ENTERPRISE$DESCRIPTION)}
</Text>
{/* Features List */}
<ul className="space-y-3 mb-6">
{ENTERPRISE_FEATURE_KEYS.map((featureKey) => (
<li key={featureKey} className="flex items-center gap-2">
<CheckCircleFillIcon className="w-4 h-4 text-blue-400 flex-shrink-0" />
<Text className="text-sm text-gray-300">{t(featureKey)}</Text>
</li>
))}
</ul>
{/* Learn More Button */}
<a
href="https://openhands.dev/enterprise"
target="_blank"
rel="noopener noreferrer"
onClick={handleLearnMore}
aria-label={t(I18nKey.ENTERPRISE$LEARN_MORE_ARIA)}
className="block w-full py-2.5 px-4 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-medium transition-colors text-center"
>
{t(I18nKey.ENTERPRISE$LEARN_MORE)}
</a>
</div>
);
}

View File

@@ -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",
}

View File

@@ -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": "Будь ласка, увійдіть, щоб авторизувати свій пристрій."
}
}

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 279 B

View File

@@ -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() {
)}
</div>
<h2 className="text-xl font-semibold mb-2">
{verificationResult.success ? "Success!" : "Error"}
{verificationResult.success
? t(I18nKey.DEVICE$SUCCESS_TITLE)
: t(I18nKey.DEVICE$ERROR_TITLE)}
</h2>
<p className="text-muted-foreground mb-4">
{verificationResult.message}
{t(verificationResult.messageKey)}
</p>
{!verificationResult.success && (
<button
@@ -116,7 +121,7 @@ export default function DeviceVerify() {
onClick={() => 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)}
</button>
)}
</div>
@@ -133,7 +138,7 @@ export default function DeviceVerify() {
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4" />
<p className="text-muted-foreground">
Processing device verification...
{t(I18nKey.DEVICE$PROCESSING)}
</p>
</div>
</div>
@@ -144,63 +149,56 @@ export default function DeviceVerify() {
// Show device authorization confirmation if user is authenticated and code is provided
if (isAuthed && userCode) {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="max-w-md w-full mx-auto p-6 bg-card rounded-lg shadow-lg">
<h1 className="text-2xl font-bold mb-4 text-center">
Device Authorization Request
</h1>
<div className="mb-6 p-4 bg-muted rounded-lg">
<p className="text-sm text-muted-foreground mb-2">Device Code:</p>
<p className="text-lg font-mono font-semibold text-center tracking-wider">
{userCode}
<div className="min-h-screen flex items-center justify-center bg-background p-4">
<div
className={`flex flex-col lg:flex-row items-center lg:items-start gap-6 w-full ${showEnterpriseBanner ? "max-w-4xl" : "max-w-md"}`}
>
{/* Device Authorization Card */}
<div
className={`flex-1 min-w-0 max-w-md w-full mx-auto p-6 bg-card rounded-lg shadow-lg border border-neutral-700 ${showEnterpriseBanner ? "lg:mx-0" : ""}`}
>
<H1 className="text-2xl mb-4 text-center">
{t(I18nKey.DEVICE$AUTHORIZATION_REQUEST)}
</H1>
<div className="mb-6 p-4 bg-neutral-900 rounded-lg border border-neutral-700">
<p className="text-xs text-neutral-500 mb-2 text-center uppercase tracking-wider">
{t(I18nKey.DEVICE$CODE_LABEL)}
</p>
<p className="text-xl font-mono font-semibold text-center tracking-[0.3em]">
{userCode}
</p>
</div>
<div className="mb-6 p-4 bg-amber-950/50 border-l-2 border-amber-500 rounded-r-lg">
<p className="text-sm font-medium text-amber-500 mb-1">
{t(I18nKey.DEVICE$SECURITY_NOTICE)}
</p>
<p className="text-sm text-gray-400">
{t(I18nKey.DEVICE$SECURITY_WARNING)}
</p>
</div>
<p className="text-muted-foreground mb-6 text-center">
{t(I18nKey.DEVICE$CONFIRM_PROMPT)}
</p>
</div>
<div className="mb-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="flex items-start">
<svg
className="w-5 h-5 text-yellow-600 mt-0.5 mr-2 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
<div className="flex gap-3">
<button
type="button"
onClick={() => window.close()}
className="flex-1 px-4 py-2 border border-neutral-600 rounded-md hover:bg-muted text-gray-300"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
<div>
<p className="text-sm font-medium text-yellow-800 mb-1">
Security Notice
</p>
<p className="text-sm text-yellow-700">
Only authorize this device if you initiated this request from
your CLI or application.
</p>
</div>
{t(I18nKey.DEVICE$CANCEL)}
</button>
<button
type="button"
onClick={() => processDeviceVerification(userCode)}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
{t(I18nKey.DEVICE$AUTHORIZE)}
</button>
</div>
</div>
<p className="text-muted-foreground mb-6 text-center">
Do you want to authorize this device to access your OpenHands
account?
</p>
<div className="flex gap-3">
<button
type="button"
onClick={() => window.close()}
className="flex-1 px-4 py-2 border border-input rounded-md hover:bg-muted"
>
Cancel
</button>
<button
type="button"
onClick={() => processDeviceVerification(userCode)}
className="flex-1 px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
>
Authorize Device
</button>
</div>
{/* Enterprise Banner */}
{showEnterpriseBanner && <EnterpriseBanner />}
</div>
</div>
);
@@ -211,11 +209,11 @@ export default function DeviceVerify() {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="max-w-md w-full mx-auto p-6 bg-card rounded-lg shadow-lg">
<h1 className="text-2xl font-bold mb-4 text-center">
Device Authorization
</h1>
<H1 className="text-2xl mb-4 text-center">
{t(I18nKey.DEVICE$AUTHORIZATION_TITLE)}
</H1>
<p className="text-muted-foreground mb-6 text-center">
Enter the code displayed on your device:
{t(I18nKey.DEVICE$ENTER_CODE_PROMPT)}
</p>
<form onSubmit={handleManualSubmit}>
<div className="mb-4">
@@ -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)}
</label>
<input
type="text"
@@ -231,14 +229,14 @@ export default function DeviceVerify() {
name="user_code"
required
className="w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring"
placeholder="Enter your device code"
placeholder={t(I18nKey.DEVICE$CODE_PLACEHOLDER)}
/>
</div>
<button
type="submit"
className="w-full px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
>
Continue
{t(I18nKey.DEVICE$CONTINUE)}
</button>
</form>
</div>
@@ -253,7 +251,7 @@ export default function DeviceVerify() {
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4" />
<p className="text-muted-foreground">
Processing device verification...
{t(I18nKey.DEVICE$PROCESSING)}
</p>
</div>
</div>
@@ -264,9 +262,9 @@ export default function DeviceVerify() {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="max-w-md w-full mx-auto p-6 bg-card rounded-lg shadow-lg text-center">
<h1 className="text-2xl font-bold mb-4">Authentication Required</h1>
<H1 className="text-2xl mb-4">{t(I18nKey.DEVICE$AUTH_REQUIRED)}</H1>
<p className="text-muted-foreground">
Please sign in to authorize your device.
{t(I18nKey.DEVICE$SIGN_IN_PROMPT)}
</p>
</div>
</div>

View File

@@ -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");