feat(frontend): redirect form submission to login page with modal

- Change login-cta Learn More button to navigate to /onboarding/information-request
- Redirect form submission to /login instead of homepage
- Move RequestSubmittedModal from home.tsx to login.tsx
- Update tests to follow conventions (use createRoutesStub)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
openhands
2026-03-19 20:22:57 +00:00
parent 05cfa59a3c
commit 529f1d854e
6 changed files with 55 additions and 51 deletions

View File

@@ -1,6 +1,7 @@
import { render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { createRoutesStub } from "react-router";
import { LoginCTA } from "#/components/features/auth/login-cta";
// Mock useTracking hook
@@ -16,8 +17,23 @@ describe("LoginCTA", () => {
vi.clearAllMocks();
});
const renderWithRouter = () => {
const Stub = createRoutesStub([
{
path: "/",
Component: LoginCTA,
},
{
path: "/onboarding/information-request",
Component: () => <div data-testid="information-request-page" />,
},
]);
return render(<Stub initialEntries={["/"]} />);
};
it("should render enterprise CTA with title and description", () => {
render(<LoginCTA />);
renderWithRouter();
expect(screen.getByTestId("login-cta")).toBeInTheDocument();
expect(screen.getByText("CTA$ENTERPRISE")).toBeInTheDocument();
@@ -25,7 +41,7 @@ describe("LoginCTA", () => {
});
it("should render all enterprise feature list items", () => {
render(<LoginCTA />);
renderWithRouter();
expect(screen.getByText("CTA$FEATURE_ON_PREMISES")).toBeInTheDocument();
expect(screen.getByText("CTA$FEATURE_DATA_CONTROL")).toBeInTheDocument();
@@ -33,31 +49,18 @@ describe("LoginCTA", () => {
expect(screen.getByText("CTA$FEATURE_SUPPORT")).toBeInTheDocument();
});
it("should render Learn More as a link with correct href and target", () => {
render(<LoginCTA />);
const learnMoreLink = screen.getByRole("link", {
name: "CTA$LEARN_MORE",
});
expect(learnMoreLink).toHaveAttribute(
"href",
"https://openhands.dev/enterprise/",
);
expect(learnMoreLink).toHaveAttribute("target", "_blank");
expect(learnMoreLink).toHaveAttribute("rel", "noopener noreferrer");
});
it("should call trackSaasSelfhostedInquiry with location 'login_page' when Learn More is clicked", async () => {
it("should track and navigate to information request page when Learn More is clicked", async () => {
const user = userEvent.setup();
render(<LoginCTA />);
renderWithRouter();
const learnMoreLink = screen.getByRole("link", {
const learnMoreButton = screen.getByRole("button", {
name: "CTA$LEARN_MORE",
});
await user.click(learnMoreLink);
await user.click(learnMoreButton);
expect(mockTrackSaasSelfhostedInquiry).toHaveBeenCalledWith({
location: "login_page",
});
expect(screen.getByTestId("information-request-page")).toBeInTheDocument();
});
});

View File

@@ -208,7 +208,7 @@ describe("InformationRequestForm", () => {
expect(mockTrackEnterpriseLeadFormSubmitted).not.toHaveBeenCalled();
});
it("should navigate to homepage with modal state when form is submitted with all fields filled", async () => {
it("should navigate to login page with modal state when form is submitted with all fields filled", async () => {
const user = userEvent.setup();
renderWithRouter();
@@ -220,7 +220,7 @@ describe("InformationRequestForm", () => {
const submitButton = screen.getByRole("button", { name: "ENTERPRISE$FORM_SUBMIT" });
await user.click(submitButton);
expect(mockNavigate).toHaveBeenCalledWith("/", {
expect(mockNavigate).toHaveBeenCalledWith("/login", {
state: { showRequestSubmittedModal: true },
});
});

View File

@@ -1,4 +1,5 @@
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
import { Card } from "#/ui/card";
import { CardTitle } from "#/ui/card-title";
import { Typography } from "#/ui/typography";
@@ -9,10 +10,12 @@ import { useTracking } from "#/hooks/use-tracking";
export function LoginCTA() {
const { t } = useTranslation();
const navigate = useNavigate();
const { trackSaasSelfhostedInquiry } = useTracking();
const handleLearnMoreClick = () => {
trackSaasSelfhostedInquiry({ location: "login_page" });
navigate("/onboarding/information-request");
};
return (
@@ -44,10 +47,8 @@ export function LoginCTA() {
</ul>
<div className={cn("h-10 flex justify-start")}>
<a
href="https://openhands.dev/enterprise/"
target="_blank"
rel="noopener noreferrer"
<button
type="button"
onClick={handleLearnMoreClick}
className={cn(
"inline-flex items-center justify-center",
@@ -58,7 +59,7 @@ export function LoginCTA() {
)}
>
{t(I18nKey.CTA$LEARN_MORE)}
</a>
</button>
</div>
</div>
</Card>

View File

@@ -62,8 +62,8 @@ export function InformationRequestForm({
message: formData.message.trim(),
});
// Navigate to homepage with state to show confirmation modal
navigate("/", { state: { showRequestSubmittedModal: true } });
// Navigate to login page with state to show confirmation modal
navigate("/login", { state: { showRequestSubmittedModal: true } });
};
const isSaas = requestType === "saas";

View File

@@ -1,5 +1,5 @@
import React from "react";
import { PrefetchPageLinks, useLocation, useNavigate } from "react-router";
import { PrefetchPageLinks } from "react-router";
import { HomeHeader } from "#/components/features/home/home-header/home-header";
import { RepoConnector } from "#/components/features/home/repo-connector";
import { TaskSuggestions } from "#/components/features/home/tasks/task-suggestions";
@@ -7,22 +7,14 @@ import { GitRepository } from "#/types/git";
import { NewConversation } from "#/components/features/home/new-conversation/new-conversation";
import { RecentConversations } from "#/components/features/home/recent-conversations/recent-conversations";
import { HomepageCTA } from "#/components/features/home/homepage-cta";
import { RequestSubmittedModal } from "#/components/features/onboarding/request-submitted-modal";
import { isCTADismissed } from "#/utils/local-storage";
import { useConfig } from "#/hooks/query/use-config";
import { ENABLE_PROJ_USER_JOURNEY } from "#/utils/feature-flags";
<PrefetchPageLinks page="/conversations/:conversationId" />;
interface LocationState {
showRequestSubmittedModal?: boolean;
}
function HomeScreen() {
const { data: config } = useConfig();
const location = useLocation();
const navigate = useNavigate();
const locationState = location.state as LocationState | null;
const [selectedRepo, setSelectedRepo] = React.useState<GitRepository | null>(
null,
@@ -32,18 +24,8 @@ function HomeScreen() {
() => !isCTADismissed("homepage"),
);
const [showRequestModal, setShowRequestModal] = React.useState(
() => locationState?.showRequestSubmittedModal ?? false,
);
const isSaasMode = config?.app_mode === "saas";
const handleModalClose = () => {
setShowRequestModal(false);
// Clear the state from location to prevent modal showing on refresh
navigate(location.pathname, { replace: true, state: {} });
};
return (
<div
data-testid="home-screen"
@@ -76,8 +58,6 @@ function HomeScreen() {
<HomepageCTA setShouldShowCTA={setShouldShowCTA} />
</div>
)}
{showRequestModal && <RequestSubmittedModal onClose={handleModalClose} />}
</div>
);
}

View File

@@ -1,5 +1,5 @@
import React from "react";
import { useNavigate, useSearchParams } from "react-router";
import { useNavigate, useSearchParams, useLocation } from "react-router";
import { useIsAuthed } from "#/hooks/query/use-is-authed";
import { useConfig } from "#/hooks/query/use-config";
import { useGitHubAuthUrl } from "#/hooks/use-github-auth-url";
@@ -7,11 +7,18 @@ import { useEmailVerification } from "#/hooks/use-email-verification";
import { useInvitation } from "#/hooks/use-invitation";
import { LoginContent } from "#/components/features/auth/login-content";
import { EmailVerificationModal } from "#/components/features/waitlist/email-verification-modal";
import { RequestSubmittedModal } from "#/components/features/onboarding/request-submitted-modal";
interface LocationState {
showRequestSubmittedModal?: boolean;
}
export default function LoginPage() {
const navigate = useNavigate();
const location = useLocation();
const [searchParams] = useSearchParams();
const returnTo = searchParams.get("returnTo") || "/";
const locationState = location.state as LocationState | null;
const config = useConfig();
const { data: isAuthed, isLoading: isAuthLoading } = useIsAuthed();
@@ -32,6 +39,15 @@ export default function LoginPage() {
authUrl: config.data?.auth_url,
});
const [showRequestModal, setShowRequestModal] = React.useState(
() => locationState?.showRequestSubmittedModal ?? false,
);
const handleRequestModalClose = () => {
setShowRequestModal(false);
navigate(location.pathname, { replace: true, state: {} });
};
// Redirect OSS mode users to home
React.useEffect(() => {
if (!config.isLoading && config.data?.app_mode === "oss") {
@@ -94,6 +110,10 @@ export default function LoginPage() {
wasRateLimited={wasRateLimited}
/>
)}
{showRequestModal && (
<RequestSubmittedModal onClose={handleRequestModalClose} />
)}
</>
);
}