diff --git a/frontend/__tests__/components/chat/expandable-message.test.tsx b/frontend/__tests__/components/chat/expandable-message.test.tsx index c361c35460..965d41da83 100644 --- a/frontend/__tests__/components/chat/expandable-message.test.tsx +++ b/frontend/__tests__/components/chat/expandable-message.test.tsx @@ -1,8 +1,9 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { screen } from "@testing-library/react"; import { renderWithProviders } from "test-utils"; +import { createRoutesStub } from "react-router"; import { ExpandableMessage } from "#/components/features/chat/expandable-message"; -import { vi } from "vitest" +import OpenHands from "#/api/open-hands"; vi.mock("react-i18next", async () => { const actual = await vi.importActual("react-i18next"); @@ -48,7 +49,7 @@ describe("ExpandableMessage", () => { id="OBSERVATION_MESSAGE$RUN" message="Command executed successfully" type="action" - success={true} + success />, ); const element = screen.getByText("OBSERVATION_MESSAGE$RUN"); @@ -93,4 +94,31 @@ describe("ExpandableMessage", () => { expect(container).toHaveClass("border-neutral-300"); expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument(); }); + + it("should render the out of credits message when the user is out of credits", async () => { + const getConfigSpy = vi.spyOn(OpenHands, "getConfig"); + // @ts-expect-error - We only care about the APP_MODE and FEATURE_FLAGS fields + getConfigSpy.mockResolvedValue({ + APP_MODE: "saas", + FEATURE_FLAGS: { + ENABLE_BILLING: true, + HIDE_LLM_SETTINGS: false, + }, + }); + const RouterStub = createRoutesStub([ + { + Component: () => ( + + ), + path: "/", + }, + ]); + + renderWithProviders(); + await screen.findByTestId("out-of-credits"); + }); }); diff --git a/frontend/__tests__/components/features/github/github-repo-selector.test.tsx b/frontend/__tests__/components/features/github/github-repo-selector.test.tsx index 783bc82020..8f4fe1d30b 100644 --- a/frontend/__tests__/components/features/github/github-repo-selector.test.tsx +++ b/frontend/__tests__/components/features/github/github-repo-selector.test.tsx @@ -30,6 +30,10 @@ describe("GitHubRepositorySelector", () => { APP_SLUG: "openhands", GITHUB_CLIENT_ID: "test-client-id", POSTHOG_CLIENT_KEY: "test-posthog-key", + FEATURE_FLAGS: { + ENABLE_BILLING: false, + HIDE_LLM_SETTINGS: false, + }, }); renderWithProviders( diff --git a/frontend/__tests__/components/features/payment/payment-form.test.tsx b/frontend/__tests__/components/features/payment/payment-form.test.tsx index 3c0ad33fc8..a4023748d4 100644 --- a/frontend/__tests__/components/features/payment/payment-form.test.tsx +++ b/frontend/__tests__/components/features/payment/payment-form.test.tsx @@ -4,10 +4,8 @@ import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, test, vi } from "vitest"; import OpenHands from "#/api/open-hands"; import { PaymentForm } from "#/components/features/payment/payment-form"; -import * as featureFlags from "#/utils/feature-flags"; describe("PaymentForm", () => { - const billingSettingsSpy = vi.spyOn(featureFlags, "BILLING_SETTINGS"); const getBalanceSpy = vi.spyOn(OpenHands, "getBalance"); const createCheckoutSessionSpy = vi.spyOn(OpenHands, "createCheckoutSession"); const getConfigSpy = vi.spyOn(OpenHands, "getConfig"); @@ -22,13 +20,16 @@ describe("PaymentForm", () => { }); beforeEach(() => { - // useBalance hook will return the balance only if the APP_MODE is "saas" + // useBalance hook will return the balance only if the APP_MODE is "saas" and the billing feature is enabled getConfigSpy.mockResolvedValue({ APP_MODE: "saas", GITHUB_CLIENT_ID: "123", POSTHOG_CLIENT_KEY: "456", + FEATURE_FLAGS: { + ENABLE_BILLING: true, + HIDE_LLM_SETTINGS: false, + }, }); - billingSettingsSpy.mockReturnValue(true); }); afterEach(() => { diff --git a/frontend/__tests__/routes/_oh.test.tsx b/frontend/__tests__/routes/_oh.test.tsx index 19efa0f67b..c010831616 100644 --- a/frontend/__tests__/routes/_oh.test.tsx +++ b/frontend/__tests__/routes/_oh.test.tsx @@ -69,6 +69,10 @@ describe("frontend/routes/_oh", () => { APP_MODE: "oss", GITHUB_CLIENT_ID: "test-id", POSTHOG_CLIENT_KEY: "test-key", + FEATURE_FLAGS: { + ENABLE_BILLING: false, + HIDE_LLM_SETTINGS: false, + }, }); // @ts-expect-error - We only care about the user_consents_to_analytics field @@ -100,6 +104,10 @@ describe("frontend/routes/_oh", () => { APP_MODE: "saas", GITHUB_CLIENT_ID: "test-id", POSTHOG_CLIENT_KEY: "test-key", + FEATURE_FLAGS: { + ENABLE_BILLING: false, + HIDE_LLM_SETTINGS: false, + }, }); renderWithProviders(); diff --git a/frontend/__tests__/routes/home.test.tsx b/frontend/__tests__/routes/home.test.tsx index 125019dcac..f4494794a2 100644 --- a/frontend/__tests__/routes/home.test.tsx +++ b/frontend/__tests__/routes/home.test.tsx @@ -8,7 +8,6 @@ import MainApp from "#/routes/_oh/route"; import SettingsScreen from "#/routes/settings"; import Home from "#/routes/_oh._index/route"; import OpenHands from "#/api/open-hands"; -import * as FeatureFlags from "#/utils/feature-flags"; const createAxiosNotFoundErrorObject = () => new AxiosError( @@ -119,8 +118,6 @@ describe("Settings 404", () => { }); it("should not open the settings modal if GET /settings fails but is SaaS mode", async () => { - // TODO: Remove HIDE_LLM_SETTINGS check once released - vi.spyOn(FeatureFlags, "HIDE_LLM_SETTINGS").mockReturnValue(true); // @ts-expect-error - we only need APP_MODE for this test getConfigSpy.mockResolvedValue({ APP_MODE: "saas" }); const error = createAxiosNotFoundErrorObject(); @@ -146,14 +143,19 @@ describe("Setup Payment modal", () => { // @ts-expect-error - we only need the APP_MODE for this test getConfigSpy.mockResolvedValue({ APP_MODE: "saas", + FEATURE_FLAGS: { + ENABLE_BILLING: true, + HIDE_LLM_SETTINGS: false, + }, }); - vi.spyOn(FeatureFlags, "BILLING_SETTINGS").mockReturnValue(true); const error = createAxiosNotFoundErrorObject(); getSettingsSpy.mockRejectedValue(error); renderWithProviders(); - const setupPaymentModal = await screen.findByTestId("proceed-to-stripe-button"); + const setupPaymentModal = await screen.findByTestId( + "proceed-to-stripe-button", + ); expect(setupPaymentModal).toBeInTheDocument(); }); }); diff --git a/frontend/__tests__/routes/settings-with-payment.test.tsx b/frontend/__tests__/routes/settings-with-payment.test.tsx index 69f9b8c364..d83c8cb695 100644 --- a/frontend/__tests__/routes/settings-with-payment.test.tsx +++ b/frontend/__tests__/routes/settings-with-payment.test.tsx @@ -6,11 +6,9 @@ import { renderWithProviders } from "test-utils"; import OpenHands from "#/api/open-hands"; import SettingsScreen from "#/routes/settings"; import { PaymentForm } from "#/components/features/payment/payment-form"; -import * as FeatureFlags from "#/utils/feature-flags"; describe("Settings Billing", () => { const getConfigSpy = vi.spyOn(OpenHands, "getConfig"); - vi.spyOn(FeatureFlags, "BILLING_SETTINGS").mockReturnValue(true); const RoutesStub = createRoutesStub([ { @@ -37,6 +35,10 @@ describe("Settings Billing", () => { APP_MODE: "oss", GITHUB_CLIENT_ID: "123", POSTHOG_CLIENT_KEY: "456", + FEATURE_FLAGS: { + ENABLE_BILLING: false, + HIDE_LLM_SETTINGS: false, + }, }); renderSettingsScreen(); @@ -52,6 +54,10 @@ describe("Settings Billing", () => { APP_MODE: "saas", GITHUB_CLIENT_ID: "123", POSTHOG_CLIENT_KEY: "456", + FEATURE_FLAGS: { + ENABLE_BILLING: true, + HIDE_LLM_SETTINGS: false, + }, }); renderSettingsScreen(); @@ -69,6 +75,10 @@ describe("Settings Billing", () => { APP_MODE: "saas", GITHUB_CLIENT_ID: "123", POSTHOG_CLIENT_KEY: "456", + FEATURE_FLAGS: { + ENABLE_BILLING: true, + HIDE_LLM_SETTINGS: false, + }, }); renderSettingsScreen(); diff --git a/frontend/__tests__/routes/settings.test.tsx b/frontend/__tests__/routes/settings.test.tsx index f06c228390..aa14bc6141 100644 --- a/frontend/__tests__/routes/settings.test.tsx +++ b/frontend/__tests__/routes/settings.test.tsx @@ -1,15 +1,6 @@ import { render, screen, waitFor, within } from "@testing-library/react"; import { createRoutesStub } from "react-router"; -import { - afterEach, - beforeAll, - beforeEach, - describe, - expect, - it, - test, - vi, -} from "vitest"; +import { afterEach, beforeEach, describe, expect, it, test, vi } from "vitest"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import userEvent, { UserEvent } from "@testing-library/user-event"; import OpenHands from "#/api/open-hands"; @@ -20,7 +11,6 @@ import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers"; import { PostApiSettings } from "#/types/settings"; import * as ConsentHandlers from "#/utils/handle-capture-consent"; import AccountSettings from "#/routes/account-settings"; -import * as FeatureFlags from "#/utils/feature-flags"; const toggleAdvancedSettings = async (user: UserEvent) => { const advancedSwitch = await screen.findByTestId("advanced-settings-switch"); @@ -39,11 +29,6 @@ describe("Settings Screen", () => { useAppLogout: vi.fn().mockReturnValue({ handleLogout: handleLogoutMock }), })); - beforeAll(() => { - // TODO: Remove this once we release - vi.spyOn(FeatureFlags, "HIDE_LLM_SETTINGS").mockReturnValue(true); - }); - afterEach(() => { vi.clearAllMocks(); }); @@ -87,6 +72,10 @@ describe("Settings Screen", () => { APP_MODE: "oss", GITHUB_CLIENT_ID: "123", POSTHOG_CLIENT_KEY: "456", + FEATURE_FLAGS: { + ENABLE_BILLING: false, + HIDE_LLM_SETTINGS: false, + }, }); }); @@ -206,6 +195,10 @@ describe("Settings Screen", () => { APP_MODE: "oss", GITHUB_CLIENT_ID: "123", POSTHOG_CLIENT_KEY: "456", + FEATURE_FLAGS: { + ENABLE_BILLING: false, + HIDE_LLM_SETTINGS: false, + }, }); renderSettingsScreen(); @@ -220,6 +213,10 @@ describe("Settings Screen", () => { GITHUB_CLIENT_ID: "123", POSTHOG_CLIENT_KEY: "456", APP_SLUG: "test-app", + FEATURE_FLAGS: { + ENABLE_BILLING: false, + HIDE_LLM_SETTINGS: false, + }, }); renderSettingsScreen(); @@ -231,6 +228,10 @@ describe("Settings Screen", () => { APP_MODE: "saas", GITHUB_CLIENT_ID: "123", POSTHOG_CLIENT_KEY: "456", + FEATURE_FLAGS: { + ENABLE_BILLING: false, + HIDE_LLM_SETTINGS: false, + }, }); renderSettingsScreen(); @@ -308,6 +309,10 @@ describe("Settings Screen", () => { APP_MODE: "oss", GITHUB_CLIENT_ID: "123", POSTHOG_CLIENT_KEY: "456", + FEATURE_FLAGS: { + ENABLE_BILLING: false, + HIDE_LLM_SETTINGS: false, + }, }); }); @@ -449,6 +454,10 @@ describe("Settings Screen", () => { APP_MODE: "oss", GITHUB_CLIENT_ID: "123", POSTHOG_CLIENT_KEY: "456", + FEATURE_FLAGS: { + ENABLE_BILLING: false, + HIDE_LLM_SETTINGS: false, + }, }); renderSettingsScreen(); @@ -463,6 +472,10 @@ describe("Settings Screen", () => { APP_MODE: "saas", GITHUB_CLIENT_ID: "123", POSTHOG_CLIENT_KEY: "456", + FEATURE_FLAGS: { + ENABLE_BILLING: false, + HIDE_LLM_SETTINGS: false, + }, }); renderSettingsScreen(); @@ -474,6 +487,10 @@ describe("Settings Screen", () => { APP_MODE: "saas", GITHUB_CLIENT_ID: "123", POSTHOG_CLIENT_KEY: "456", + FEATURE_FLAGS: { + ENABLE_BILLING: false, + HIDE_LLM_SETTINGS: false, + }, }); getSettingsSpy.mockResolvedValue({ @@ -492,6 +509,10 @@ describe("Settings Screen", () => { APP_MODE: "saas", GITHUB_CLIENT_ID: "123", POSTHOG_CLIENT_KEY: "456", + FEATURE_FLAGS: { + ENABLE_BILLING: false, + HIDE_LLM_SETTINGS: false, + }, }); renderSettingsScreen(); @@ -506,6 +527,10 @@ describe("Settings Screen", () => { APP_MODE: "saas", GITHUB_CLIENT_ID: "123", POSTHOG_CLIENT_KEY: "456", + FEATURE_FLAGS: { + ENABLE_BILLING: false, + HIDE_LLM_SETTINGS: false, + }, }); getSettingsSpy.mockResolvedValue({ @@ -982,6 +1007,10 @@ describe("Settings Screen", () => { APP_MODE: "saas", GITHUB_CLIENT_ID: "123", POSTHOG_CLIENT_KEY: "456", + FEATURE_FLAGS: { + ENABLE_BILLING: false, + HIDE_LLM_SETTINGS: true, + }, }); }); diff --git a/frontend/__tests__/utils/test-config.tsx b/frontend/__tests__/utils/test-config.tsx deleted file mode 100644 index f6a0971697..0000000000 --- a/frontend/__tests__/utils/test-config.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { vi } from "vitest"; -import OpenHands from "#/api/open-hands"; - -export const setupTestConfig = () => { - const getConfigSpy = vi.spyOn(OpenHands, "getConfig"); - getConfigSpy.mockResolvedValue({ - APP_MODE: "oss", - GITHUB_CLIENT_ID: "test-id", - POSTHOG_CLIENT_KEY: "test-key", - }); -}; - -export const setupSaasTestConfig = () => { - const getConfigSpy = vi.spyOn(OpenHands, "getConfig"); - getConfigSpy.mockResolvedValue({ - APP_MODE: "saas", - GITHUB_CLIENT_ID: "test-id", - POSTHOG_CLIENT_KEY: "test-key", - }); -}; diff --git a/frontend/src/api/open-hands.types.ts b/frontend/src/api/open-hands.types.ts index c01764204b..74c3aa180a 100644 --- a/frontend/src/api/open-hands.types.ts +++ b/frontend/src/api/open-hands.types.ts @@ -49,6 +49,10 @@ export interface GetConfigResponse { GITHUB_CLIENT_ID: string; POSTHOG_CLIENT_KEY: string; STRIPE_PUBLISHABLE_KEY?: string; + FEATURE_FLAGS: { + ENABLE_BILLING: boolean; + HIDE_LLM_SETTINGS: boolean; + }; } export interface GetVSCodeUrlResponse { diff --git a/frontend/src/components/features/chat/expandable-message.tsx b/frontend/src/components/features/chat/expandable-message.tsx index fa69a1a94a..8c929492e9 100644 --- a/frontend/src/components/features/chat/expandable-message.tsx +++ b/frontend/src/components/features/chat/expandable-message.tsx @@ -11,7 +11,6 @@ import CheckCircle from "#/icons/check-circle-solid.svg?react"; import XCircle from "#/icons/x-circle-solid.svg?react"; import { cn } from "#/utils/utils"; import { useConfig } from "#/hooks/query/use-config"; -import { BILLING_SETTINGS } from "#/utils/feature-flags"; interface ExpandableMessageProps { id?: string; @@ -43,12 +42,15 @@ export function ExpandableMessage({ const statusIconClasses = "h-4 w-4 ml-2 inline"; if ( - BILLING_SETTINGS() && + config?.FEATURE_FLAGS.ENABLE_BILLING && config?.APP_MODE === "saas" && id === "STATUS$ERROR_LLM_OUT_OF_CREDITS" ) { return ( -
+
{t("STATUS$ERROR_LLM_OUT_OF_CREDITS")} diff --git a/frontend/src/components/features/sidebar/sidebar.tsx b/frontend/src/components/features/sidebar/sidebar.tsx index b1f39a8120..6055a295c4 100644 --- a/frontend/src/components/features/sidebar/sidebar.tsx +++ b/frontend/src/components/features/sidebar/sidebar.tsx @@ -21,7 +21,6 @@ import { useLogout } from "#/hooks/mutation/use-logout"; import { useConfig } from "#/hooks/query/use-config"; import { cn } from "#/utils/utils"; import { displayErrorToast } from "#/utils/custom-toast-handlers"; -import { HIDE_LLM_SETTINGS } from "#/utils/feature-flags"; import { useSaveSettings } from "#/hooks/mutation/use-save-settings"; export function Sidebar() { @@ -45,10 +44,11 @@ export function Sidebar() { React.useState(false); // TODO: Remove HIDE_LLM_SETTINGS check once released - const isSaas = HIDE_LLM_SETTINGS() && config?.APP_MODE === "saas"; + const shouldHideLlmSettings = + config?.FEATURE_FLAGS.HIDE_LLM_SETTINGS && config?.APP_MODE === "saas"; React.useEffect(() => { - if (isSaas) return; + if (shouldHideLlmSettings) return; if (location.pathname === "/settings") { setSettingsModalIsOpen(false); diff --git a/frontend/src/hooks/query/use-balance.ts b/frontend/src/hooks/query/use-balance.ts index 2ac2168af6..8c0e7251da 100644 --- a/frontend/src/hooks/query/use-balance.ts +++ b/frontend/src/hooks/query/use-balance.ts @@ -1,7 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { useConfig } from "./use-config"; import OpenHands from "#/api/open-hands"; -import { BILLING_SETTINGS } from "#/utils/feature-flags"; export const useBalance = () => { const { data: config } = useConfig(); @@ -9,6 +8,7 @@ export const useBalance = () => { return useQuery({ queryKey: ["user", "balance"], queryFn: OpenHands.getBalance, - enabled: config?.APP_MODE === "saas" && BILLING_SETTINGS(), + enabled: + config?.APP_MODE === "saas" && config?.FEATURE_FLAGS.ENABLE_BILLING, }); }; diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index b3caacd210..e1d170f1fe 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -128,7 +128,6 @@ const openHandsHandlers = [ const url = new URL(request.url); const file = url.searchParams.get("file")?.toString(); - if (file) { return HttpResponse.json({ code: `Content of ${file}` }); } @@ -181,6 +180,10 @@ export const handlers = [ GITHUB_CLIENT_ID: "fake-github-client-id", POSTHOG_CLIENT_KEY: "fake-posthog-client-key", STRIPE_PUBLISHABLE_KEY: "", + FEATURE_FLAGS: { + ENABLE_BILLING: mockSaas, + HIDE_LLM_SETTINGS: mockSaas, + }, }; return HttpResponse.json(config); diff --git a/frontend/src/routes/_oh/route.tsx b/frontend/src/routes/_oh/route.tsx index 31f9605876..798028fdb4 100644 --- a/frontend/src/routes/_oh/route.tsx +++ b/frontend/src/routes/_oh/route.tsx @@ -20,7 +20,6 @@ import { useAuth } from "#/context/auth-context"; 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 { BILLING_SETTINGS } from "#/utils/feature-flags"; import { displaySuccessToast } from "#/utils/custom-toast-handlers"; export function ErrorBoundary() { @@ -145,7 +144,7 @@ export default function MainApp() { /> )} - {BILLING_SETTINGS() && + {config.data?.FEATURE_FLAGS.ENABLE_BILLING && config.data?.APP_MODE === "saas" && settings?.IS_NEW_USER && }
diff --git a/frontend/src/routes/account-settings.tsx b/frontend/src/routes/account-settings.tsx index 7b6fc10dc2..5dfeca6336 100644 --- a/frontend/src/routes/account-settings.tsx +++ b/frontend/src/routes/account-settings.tsx @@ -26,7 +26,6 @@ import { displaySuccessToast, } from "#/utils/custom-toast-handlers"; import { PostSettings } from "#/types/settings"; -import { HIDE_LLM_SETTINGS } from "#/utils/feature-flags"; const REMOTE_RUNTIME_OPTIONS = [ { key: 1, label: "1x (2 core, 8G)" }, @@ -53,7 +52,8 @@ function AccountSettings() { const isSuccess = isSuccessfulSettings && isSuccessfulResources; const isSaas = config?.APP_MODE === "saas"; - const shouldHandleSpecialSaasCase = HIDE_LLM_SETTINGS() && isSaas; + const shouldHandleSpecialSaasCase = + config?.FEATURE_FLAGS.HIDE_LLM_SETTINGS && isSaas; const determineWhetherToToggleAdvancedSettings = () => { if (shouldHandleSpecialSaasCase) return true; diff --git a/frontend/src/routes/billing.tsx b/frontend/src/routes/billing.tsx index c644b83f62..801d9e6ea0 100644 --- a/frontend/src/routes/billing.tsx +++ b/frontend/src/routes/billing.tsx @@ -7,12 +7,11 @@ import { displayErrorToast, displaySuccessToast, } from "#/utils/custom-toast-handlers"; -import { BILLING_SETTINGS } from "#/utils/feature-flags"; export const clientLoader = async () => { const config = queryClient.getQueryData(["config"]); - if (config?.APP_MODE !== "saas" || !BILLING_SETTINGS()) { + if (config?.APP_MODE !== "saas" || !config.FEATURE_FLAGS.ENABLE_BILLING) { return redirect("/settings"); } diff --git a/frontend/src/routes/settings.tsx b/frontend/src/routes/settings.tsx index ea66c296ff..2e25a6926c 100644 --- a/frontend/src/routes/settings.tsx +++ b/frontend/src/routes/settings.tsx @@ -2,11 +2,11 @@ import { NavLink, Outlet } from "react-router"; import SettingsIcon from "#/icons/settings.svg?react"; import { cn } from "#/utils/utils"; import { useConfig } from "#/hooks/query/use-config"; -import { BILLING_SETTINGS } from "#/utils/feature-flags"; function SettingsScreen() { const { data: config } = useConfig(); const isSaas = config?.APP_MODE === "saas"; + const billingIsEnabled = config?.FEATURE_FLAGS.ENABLE_BILLING; return (
Settings - {isSaas && BILLING_SETTINGS() && ( + {isSaas && billingIsEnabled && (