Move Terms of Service acceptance to dedicated page (#8071)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Robert Brennan <accounts@rbren.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: tofarr <tofarr@gmail.com>
Co-authored-by: Ray Myers <ray.myers@gmail.com>
Co-authored-by: மனோஜ்குமார் பழனிச்சாமி <smartmanoj42857@gmail.com>
Co-authored-by: Lenshood <lenshood.zxh@gmail.com>
Co-authored-by: OpenHands <opendevin@all-hands.dev>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
This commit is contained in:
chuckbutkus
2025-04-29 11:12:45 -04:00
committed by GitHub
parent 42eb355a68
commit d18edc8b30
16 changed files with 421 additions and 79 deletions

View File

@@ -1,12 +1,16 @@
import { render, screen } from "@testing-library/react";
import { it, describe, expect, vi, beforeAll, afterAll } from "vitest";
import { it, describe, expect, vi, beforeEach, afterEach } from "vitest";
import userEvent from "@testing-library/user-event";
import { AuthModal } from "#/components/features/waitlist/auth-modal";
import * as CaptureConsent from "#/utils/handle-capture-consent";
import * as AuthHook from "#/context/auth-context";
// Mock the useAuthUrl hook
vi.mock("#/hooks/use-auth-url", () => ({
useAuthUrl: () => "https://gitlab.com/oauth/authorize"
}));
describe("AuthModal", () => {
beforeAll(() => {
beforeEach(() => {
vi.stubGlobal("location", { href: "" });
vi.spyOn(AuthHook, "useAuth").mockReturnValue({
providersAreSet: false,
@@ -16,50 +20,29 @@ describe("AuthModal", () => {
});
});
afterAll(() => {
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
vi.resetAllMocks();
});
it("should render a tos checkbox that is unchecked by default", () => {
render(<AuthModal githubAuthUrl={null} appMode="saas" />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).not.toBeChecked();
});
it("should only enable the identity provider buttons if the tos checkbox is checked", async () => {
const user = userEvent.setup();
render(<AuthModal githubAuthUrl={null} appMode="saas" />);
const checkbox = screen.getByRole("checkbox");
it("should render the GitHub and GitLab buttons", () => {
render(<AuthModal githubAuthUrl="mock-url" appMode="saas" />);
const githubButton = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
const gitlabButton = screen.getByRole("button", { name: "GITLAB$CONNECT_TO_GITLAB" });
expect(githubButton).toBeDisabled();
expect(gitlabButton).toBeDisabled();
await user.click(checkbox);
expect(githubButton).not.toBeDisabled();
expect(gitlabButton).not.toBeDisabled();
expect(githubButton).toBeInTheDocument();
expect(gitlabButton).toBeInTheDocument();
});
it("should set user analytics consent to true when the user checks the tos checkbox", async () => {
const handleCaptureConsentSpy = vi.spyOn(
CaptureConsent,
"handleCaptureConsent",
);
it("should redirect to GitHub auth URL when GitHub button is clicked", async () => {
const user = userEvent.setup();
render(<AuthModal githubAuthUrl="mock-url" appMode="saas" />);
const mockUrl = "https://github.com/login/oauth/authorize";
render(<AuthModal githubAuthUrl={mockUrl} appMode="saas" />);
const checkbox = screen.getByRole("checkbox");
await user.click(checkbox);
const githubButton = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
await user.click(githubButton);
const button = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
await user.click(button);
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
expect(window.location.href).toBe(mockUrl);
});
});

View File

@@ -0,0 +1,136 @@
import { render, screen } from "@testing-library/react";
import { it, describe, expect, vi, beforeEach, afterEach } from "vitest";
import userEvent from "@testing-library/user-event";
import AcceptTOS from "#/routes/accept-tos";
import * as CaptureConsent from "#/utils/handle-capture-consent";
import * as ToastHandlers from "#/utils/custom-toast-handlers";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { openHands } from "#/api/open-hands-axios";
// Mock the react-router hooks
vi.mock("react-router", () => ({
useNavigate: () => vi.fn(),
useSearchParams: () => [
{
get: (param: string) => {
if (param === "redirect_url") {
return "/dashboard";
}
return null;
},
},
],
}));
// Mock the axios instance
vi.mock("#/api/open-hands-axios", () => ({
openHands: {
post: vi.fn(),
},
}));
// Mock the toast handlers
vi.mock("#/utils/custom-toast-handlers", () => ({
displayErrorToast: vi.fn(),
}));
// Create a wrapper with QueryClientProvider
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
describe("AcceptTOS", () => {
beforeEach(() => {
vi.stubGlobal("location", { href: "" });
});
afterEach(() => {
vi.unstubAllGlobals();
vi.resetAllMocks();
});
it("should render a TOS checkbox that is unchecked by default", () => {
render(<AcceptTOS />, { wrapper: createWrapper() });
const checkbox = screen.getByRole("checkbox");
const continueButton = screen.getByRole("button", { name: "TOS$CONTINUE" });
expect(checkbox).not.toBeChecked();
expect(continueButton).toBeDisabled();
});
it("should enable the continue button when the TOS checkbox is checked", async () => {
const user = userEvent.setup();
render(<AcceptTOS />, { wrapper: createWrapper() });
const checkbox = screen.getByRole("checkbox");
const continueButton = screen.getByRole("button", { name: "TOS$CONTINUE" });
expect(continueButton).toBeDisabled();
await user.click(checkbox);
expect(continueButton).not.toBeDisabled();
});
it("should set user analytics consent to true when the user accepts TOS", async () => {
const handleCaptureConsentSpy = vi.spyOn(
CaptureConsent,
"handleCaptureConsent",
);
// Mock the API response
vi.mocked(openHands.post).mockResolvedValue({
data: { redirect_url: "/dashboard" },
});
const user = userEvent.setup();
render(<AcceptTOS />, { wrapper: createWrapper() });
const checkbox = screen.getByRole("checkbox");
await user.click(checkbox);
const continueButton = screen.getByRole("button", { name: "TOS$CONTINUE" });
await user.click(continueButton);
// Wait for the mutation to complete
await new Promise(process.nextTick);
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
expect(openHands.post).toHaveBeenCalledWith("/api/accept_tos", {
redirect_url: "/dashboard",
});
});
it("should handle external redirect URLs", async () => {
// Mock the API response with an external URL
const externalUrl = "https://example.com/callback";
vi.mocked(openHands.post).mockResolvedValue({
data: { redirect_url: externalUrl },
});
const user = userEvent.setup();
render(<AcceptTOS />, { wrapper: createWrapper() });
const checkbox = screen.getByRole("checkbox");
await user.click(checkbox);
const continueButton = screen.getByRole("button", { name: "TOS$CONTINUE" });
await user.click(continueButton);
// Wait for the mutation to complete
await new Promise(process.nextTick);
expect(window.location.href).toBe(externalUrl);
});
});

View File

@@ -44,6 +44,7 @@
"react-router": "^7.5.2",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.9",
"react-toastify": "^11.0.5",
"remark-gfm": "^4.0.1",
"sirv-cli": "^3.0.1",
"socket.io-client": "^4.8.1",
@@ -15461,6 +15462,19 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-toastify": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
"integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",

View File

@@ -4,8 +4,6 @@ import { I18nKey } from "#/i18n/declaration";
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
import { TOSCheckbox } from "./tos-checkbox";
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
import { BrandButton } from "../settings/brand-button";
import GitHubLogo from "#/assets/branding/github-logo.svg?react";
import GitLabLogo from "#/assets/branding/gitlab-logo.svg?react";
@@ -19,7 +17,6 @@ interface AuthModalProps {
export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
const { t } = useTranslation();
const [isTosAccepted, setIsTosAccepted] = React.useState(false);
const gitlabAuthUrl = useAuthUrl({
appMode: appMode || null,
@@ -28,14 +25,14 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
const handleGitHubAuth = () => {
if (githubAuthUrl) {
handleCaptureConsent(true);
// Always start the OIDC flow, let the backend handle TOS check
window.location.href = githubAuthUrl;
}
};
const handleGitLabAuth = () => {
if (gitlabAuthUrl) {
handleCaptureConsent(true);
// Always start the OIDC flow, let the backend handle TOS check
window.location.href = gitlabAuthUrl;
}
};
@@ -50,11 +47,8 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
</h1>
</div>
<TOSCheckbox onChange={() => setIsTosAccepted((prev) => !prev)} />
<div className="flex flex-col gap-3 w-full">
<BrandButton
isDisabled={!isTosAccepted}
type="button"
variant="primary"
onClick={handleGitHubAuth}
@@ -65,7 +59,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
</BrandButton>
<BrandButton
isDisabled={!isTosAccepted}
type="button"
variant="primary"
onClick={handleGitLabAuth}

View File

@@ -1,14 +1,18 @@
import { useQuery } from "@tanstack/react-query";
import { useConfig } from "./use-config";
import OpenHands from "#/api/open-hands";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
export const useBalance = () => {
const { data: config } = useConfig();
const isOnTosPage = useIsOnTosPage();
return useQuery({
queryKey: ["user", "balance"],
queryFn: OpenHands.getBalance,
enabled:
config?.APP_MODE === "saas" && config?.FEATURE_FLAGS.ENABLE_BILLING,
!isOnTosPage &&
config?.APP_MODE === "saas" &&
config?.FEATURE_FLAGS.ENABLE_BILLING,
});
};

View File

@@ -1,10 +1,15 @@
import { useQuery } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
export const useConfig = () =>
useQuery({
export const useConfig = () => {
const isOnTosPage = useIsOnTosPage();
return useQuery({
queryKey: ["config"],
queryFn: OpenHands.getConfig,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
gcTime: 1000 * 60 * 15, // 15 minutes,
enabled: !isOnTosPage,
});
};

View File

@@ -3,17 +3,19 @@ import React from "react";
import OpenHands from "#/api/open-hands";
import { useConfig } from "./use-config";
import { useAuth } from "#/context/auth-context";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
export const useIsAuthed = () => {
const { providersAreSet } = useAuth();
const { data: config } = useConfig();
const isOnTosPage = useIsOnTosPage();
const appMode = React.useMemo(() => config?.APP_MODE, [config]);
return useQuery({
queryKey: ["user", "authenticated", providersAreSet, appMode],
queryFn: () => OpenHands.authenticate(appMode!),
enabled: !!appMode,
enabled: !!appMode && !isOnTosPage,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
retry: false,

View File

@@ -4,6 +4,7 @@ import posthog from "posthog-js";
import OpenHands from "#/api/open-hands";
import { useAuth } from "#/context/auth-context";
import { DEFAULT_SETTINGS } from "#/services/settings";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
import { Settings } from "#/types/settings";
const getSettingsQueryFn = async (): Promise<Settings> => {
@@ -31,6 +32,8 @@ export const useSettings = () => {
const { setProviderTokensSet, providerTokensSet, setProvidersAreSet } =
useAuth();
const isOnTosPage = useIsOnTosPage();
const query = useQuery({
queryKey: ["settings", providerTokensSet],
queryFn: getSettingsQueryFn,
@@ -40,6 +43,7 @@ export const useSettings = () => {
retry: (_, error) => error.status !== 404,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
enabled: !isOnTosPage,
meta: {
disableToast: true,
},

View File

@@ -0,0 +1,11 @@
import { useLocation } from "react-router";
/**
* Hook to check if the current page is the Terms of Service acceptance page.
*
* @returns {boolean} True if the current page is the TOS acceptance page, false otherwise.
*/
export const useIsOnTosPage = (): boolean => {
const { pathname } = useLocation();
return pathname === "/accept-tos";
};

View File

@@ -469,4 +469,8 @@ export enum I18nKey {
SYSTEM_MESSAGE_MODAL$TOOLS_TAB = "SYSTEM_MESSAGE_MODAL$TOOLS_TAB",
SYSTEM_MESSAGE_MODAL$PARAMETERS = "SYSTEM_MESSAGE_MODAL$PARAMETERS",
SYSTEM_MESSAGE_MODAL$NO_TOOLS = "SYSTEM_MESSAGE_MODAL$NO_TOOLS",
TOS$ACCEPT_TERMS_OF_SERVICE = "TOS$ACCEPT_TERMS_OF_SERVICE",
TOS$ACCEPT_TERMS_DESCRIPTION = "TOS$ACCEPT_TERMS_DESCRIPTION",
TOS$CONTINUE = "TOS$CONTINUE",
TOS$ERROR_ACCEPTING = "TOS$ERROR_ACCEPTING",
}

View File

@@ -6754,4 +6754,53 @@
"es": "No hay herramientas disponibles para este agente",
"tr": "Bu ajan için kullanılabilir araç yok"
}
,
"TOS$ACCEPT_TERMS_OF_SERVICE": {
"en": "Accept Terms of Service",
"ja": "利用規約に同意する",
"zh-CN": "接受服务条款",
"zh-TW": "接受服務條款",
"ko-KR": "서비스 약관 동의",
"fr": "Accepter les conditions d'utilisation",
"es": "Aceptar términos de servicio",
"de": "Nutzungsbedingungen akzeptieren",
"it": "Accetta i termini di servizio",
"pt": "Aceitar termos de serviço"
},
"TOS$ACCEPT_TERMS_DESCRIPTION": {
"en": "Please review and accept our terms of service before continuing",
"ja": "続行する前に利用規約を確認して同意してください",
"zh-CN": "请在继续之前查看并接受我们的服务条款",
"zh-TW": "請在繼續之前查看並接受我們的服務條款",
"ko-KR": "계속하기 전에 서비스 약관을 검토하고 동의해 주세요",
"fr": "Veuillez examiner et accepter nos conditions d'utilisation avant de continuer",
"es": "Por favor, revise y acepte nuestros términos de servicio antes de continuar",
"de": "Bitte überprüfen und akzeptieren Sie unsere Nutzungsbedingungen, bevor Sie fortfahren",
"it": "Si prega di rivedere e accettare i nostri termini di servizio prima di continuare",
"pt": "Por favor, revise e aceite nossos termos de serviço antes de continuar"
},
"TOS$CONTINUE": {
"en": "Continue",
"ja": "続行",
"zh-CN": "继续",
"zh-TW": "繼續",
"ko-KR": "계속",
"fr": "Continuer",
"es": "Continuar",
"de": "Fortfahren",
"it": "Continua",
"pt": "Continuar"
},
"TOS$ERROR_ACCEPTING": {
"en": "Error accepting Terms of Service",
"ja": "利用規約の承諾中にエラーが発生しました",
"zh-CN": "接受服务条款时出错",
"zh-TW": "接受服務條款時出錯",
"ko-KR": "서비스 약관 수락 중 오류 발생",
"fr": "Erreur lors de l'acceptation des conditions d'utilisation",
"es": "Error al aceptar los Términos de Servicio",
"de": "Fehler beim Akzeptieren der Nutzungsbedingungen",
"it": "Errore nell'accettazione dei Termini di Servizio",
"pt": "Erro ao aceitar os Termos de Serviço"
}
}

View File

@@ -8,6 +8,7 @@ import {
export default [
layout("routes/root-layout.tsx", [
index("routes/home.tsx"),
route("accept-tos", "routes/accept-tos.tsx"),
route("settings", "routes/settings.tsx", [
index("routes/llm-settings.tsx"),
route("git", "routes/git-settings.tsx"),

View File

@@ -0,0 +1,84 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router";
import { useMutation } from "@tanstack/react-query";
import { I18nKey } from "#/i18n/declaration";
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
import { TOSCheckbox } from "#/components/features/waitlist/tos-checkbox";
import { BrandButton } from "#/components/features/settings/brand-button";
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
import { openHands } from "#/api/open-hands-axios";
export default function AcceptTOS() {
const { t } = useTranslation();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [isTosAccepted, setIsTosAccepted] = React.useState(false);
// Get the redirect URL from the query parameters
const redirectUrl = searchParams.get("redirect_url") || "/";
// Use mutation for accepting TOS
const { mutate: acceptTOS, isPending: isSubmitting } = useMutation({
mutationFn: async () => {
// Set consent for analytics
handleCaptureConsent(true);
// Call the API to record TOS acceptance in the database
return openHands.post("/api/accept_tos", {
redirect_url: redirectUrl,
});
},
onSuccess: (response) => {
// Get the redirect URL from the response
const finalRedirectUrl = response.data.redirect_url || redirectUrl;
// Check if the redirect URL is an external URL (starts with http or https)
if (
finalRedirectUrl.startsWith("http://") ||
finalRedirectUrl.startsWith("https://")
) {
// For external URLs, redirect using window.location
window.location.href = finalRedirectUrl;
} else {
// For internal routes, use navigate
navigate(finalRedirectUrl);
}
},
});
const handleAcceptTOS = () => {
if (isTosAccepted && !isSubmitting) {
acceptTOS();
}
};
return (
<div className="flex flex-col items-center justify-center h-full">
<div className="border border-tertiary p-8 rounded-lg max-w-md w-full flex flex-col gap-6 items-center bg-base-secondary">
<AllHandsLogo width={68} height={46} />
<div className="flex flex-col gap-2 w-full items-center text-center">
<h1 className="text-2xl font-bold">
{t(I18nKey.TOS$ACCEPT_TERMS_OF_SERVICE)}
</h1>
<p className="text-sm text-gray-500">
{t(I18nKey.TOS$ACCEPT_TERMS_DESCRIPTION)}
</p>
</div>
<TOSCheckbox onChange={() => setIsTosAccepted((prev) => !prev)} />
<BrandButton
isDisabled={!isTosAccepted || isSubmitting}
type="button"
variant="primary"
onClick={handleAcceptTOS}
className="w-full"
>
{isSubmitting ? t(I18nKey.HOME$LOADING) : t(I18nKey.TOS$CONTINUE)}
</BrandButton>
</div>
</div>
);
}

View File

@@ -21,6 +21,7 @@ import { useMigrateUserConsent } from "#/hooks/use-migrate-user-consent";
import { useBalance } from "#/hooks/query/use-balance";
import { SetupPaymentModal } from "#/components/features/payment/setup-payment-modal";
import { displaySuccessToast } from "#/utils/custom-toast-handlers";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
export function ErrorBoundary() {
const error = useRouteError();
@@ -58,6 +59,7 @@ export function ErrorBoundary() {
export default function MainApp() {
const navigate = useNavigate();
const { pathname } = useLocation();
const tosPageStatus = useIsOnTosPage();
const [searchParams] = useSearchParams();
const { data: settings } = useSettings();
const { error, isFetching } = useBalance();
@@ -71,49 +73,75 @@ export default function MainApp() {
isError: authError,
} = useIsAuthed();
// Always call the hook, but we'll only use the result when not on TOS page
const gitHubAuthUrl = useGitHubAuthUrl({
appMode: config.data?.APP_MODE || null,
gitHubClientId: config.data?.GITHUB_CLIENT_ID || null,
});
// When on TOS page, we don't use the GitHub auth URL
const effectiveGitHubAuthUrl = tosPageStatus ? null : gitHubAuthUrl;
const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(false);
React.useEffect(() => {
if (settings?.LANGUAGE) {
// Don't change language when on TOS page
if (!tosPageStatus && settings?.LANGUAGE) {
i18n.changeLanguage(settings.LANGUAGE);
}
}, [settings?.LANGUAGE]);
}, [settings?.LANGUAGE, tosPageStatus]);
React.useEffect(() => {
const consentFormModalIsOpen =
settings?.USER_CONSENTS_TO_ANALYTICS === null;
// Don't show consent form when on TOS page
if (!tosPageStatus) {
const consentFormModalIsOpen =
settings?.USER_CONSENTS_TO_ANALYTICS === null;
setConsentFormIsOpen(consentFormModalIsOpen);
}, [settings]);
React.useEffect(() => {
// Migrate user consent to the server if it was previously stored in localStorage
migrateUserConsent({
handleAnalyticsWasPresentInLocalStorage: () => {
setConsentFormIsOpen(false);
},
});
}, []);
React.useEffect(() => {
// Don't allow users to use the app if it 402s
if (error?.status === 402 && pathname !== "/") {
navigate("/");
} else if (!isFetching && searchParams.get("free_credits") === "success") {
displaySuccessToast(t(I18nKey.BILLING$YOURE_IN));
searchParams.delete("free_credits");
navigate("/");
setConsentFormIsOpen(consentFormModalIsOpen);
}
}, [error?.status, pathname, isFetching]);
}, [settings, tosPageStatus]);
const userIsAuthed = !!isAuthed && !authError;
React.useEffect(() => {
// Don't migrate user consent when on TOS page
if (!tosPageStatus) {
// Migrate user consent to the server if it was previously stored in localStorage
migrateUserConsent({
handleAnalyticsWasPresentInLocalStorage: () => {
setConsentFormIsOpen(false);
},
});
}
}, [tosPageStatus]);
React.useEffect(() => {
// Don't do any redirects when on TOS page
if (!tosPageStatus) {
// Don't allow users to use the app if it 402s
if (error?.status === 402 && pathname !== "/") {
navigate("/");
} else if (
!isFetching &&
searchParams.get("free_credits") === "success"
) {
displaySuccessToast(t(I18nKey.BILLING$YOURE_IN));
searchParams.delete("free_credits");
navigate("/");
}
}
}, [error?.status, pathname, isFetching, tosPageStatus]);
// When on TOS page, we don't make any API calls, so we need to handle this case
const userIsAuthed = tosPageStatus ? false : !!isAuthed && !authError;
// Only show the auth modal if:
// 1. User is not authenticated
// 2. We're not currently on the TOS page
// 3. We're in SaaS mode
const renderAuthModal =
!isFetchingAuth && !userIsAuthed && config.data?.APP_MODE === "saas";
!isFetchingAuth &&
!userIsAuthed &&
!tosPageStatus &&
config.data?.APP_MODE === "saas";
return (
<div
@@ -131,7 +159,7 @@ export default function MainApp() {
{renderAuthModal && (
<AuthModal
githubAuthUrl={gitHubAuthUrl}
githubAuthUrl={effectiveGitHubAuthUrl}
appMode={config.data?.APP_MODE}
/>
)}

View File

@@ -0,0 +1,20 @@
/**
* Checks if the current page is the Terms of Service acceptance page.
* This function works outside of React Router context by checking window.location directly.
*
* @param {string} [pathname] - Optional pathname from React Router's useLocation hook
* @returns {boolean} True if the current page is the TOS acceptance page, false otherwise.
*/
export const isOnTosPage = (pathname?: string): boolean => {
// If pathname is provided (from React Router), use it
if (pathname !== undefined) {
return pathname === "/accept-tos";
}
// Otherwise check window.location (works outside React Router context)
if (typeof window !== "undefined") {
return window.location.pathname === "/accept-tos";
}
return false;
};

View File

@@ -18,6 +18,10 @@ vi.mock("react-i18next", async (importOriginal) => ({
}),
}));
vi.mock("#/hooks/use-is-on-tos-page", () => ({
useIsOnTosPage: () => false,
}));
// Mock requests during tests
beforeAll(() => server.listen({ onUnhandledRequest: "bypass" }));
afterEach(() => {