mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
feat(frontend): Display toast for first time saas users (#8279)
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { screen, waitFor, within } from "@testing-library/react";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import {
|
||||
createAxiosNotFoundErrorObject,
|
||||
renderWithProviders,
|
||||
} from "test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import MainApp from "#/routes/root-layout";
|
||||
import i18n from "#/i18n";
|
||||
import * as CaptureConsent from "#/utils/handle-capture-consent";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import * as ToastHandlers from "#/utils/custom-toast-handlers";
|
||||
|
||||
describe("frontend/routes/_oh", () => {
|
||||
const RouteStub = createRoutesStub([{ Component: MainApp, path: "/" }]);
|
||||
@@ -172,4 +176,32 @@ describe("frontend/routes/_oh", () => {
|
||||
// expect(logoutCleanupSpy).toHaveBeenCalled();
|
||||
expect(localStorage.getItem("ghToken")).toBeNull();
|
||||
});
|
||||
|
||||
it("should render a you're in toast if it is a new user and in saas mode", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const displaySuccessToastSpy = vi.spyOn(
|
||||
ToastHandlers,
|
||||
"displaySuccessToast",
|
||||
);
|
||||
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "test-id",
|
||||
POSTHOG_CLIENT_KEY: "test-key",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
});
|
||||
|
||||
getSettingsSpy.mockRejectedValue(createAxiosNotFoundErrorObject());
|
||||
|
||||
renderWithProviders(<RouteStub />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(displaySuccessToastSpy).toHaveBeenCalledWith("BILLING$YOURE_IN");
|
||||
expect(displaySuccessToastSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,30 +4,13 @@ import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { Provider } from "react-redux";
|
||||
import { setupStore } from "test-utils";
|
||||
import { AxiosError } from "axios";
|
||||
import { createAxiosNotFoundErrorObject, setupStore } from "test-utils";
|
||||
import HomeScreen from "#/routes/home";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import MainApp from "#/routes/root-layout";
|
||||
|
||||
const createAxiosNotFoundErrorObject = () =>
|
||||
new AxiosError(
|
||||
"Request failed with status code 404",
|
||||
"ERR_BAD_REQUEST",
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
status: 404,
|
||||
statusText: "Not Found",
|
||||
data: { message: "Settings not found" },
|
||||
headers: {},
|
||||
// @ts-expect-error - we only need the response object for this test
|
||||
config: {},
|
||||
},
|
||||
);
|
||||
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
Component: MainApp,
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
Outlet,
|
||||
useNavigate,
|
||||
useLocation,
|
||||
useSearchParams,
|
||||
} from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
@@ -60,9 +59,8 @@ export default function MainApp() {
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const tosPageStatus = useIsOnTosPage();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { data: settings } = useSettings();
|
||||
const { error, isFetching } = useBalance();
|
||||
const { error } = useBalance();
|
||||
const { migrateUserConsent } = useMigrateUserConsent();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -114,21 +112,18 @@ export default function MainApp() {
|
||||
}, [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("/");
|
||||
}
|
||||
if (settings?.IS_NEW_USER && config.data?.APP_MODE === "saas") {
|
||||
displaySuccessToast(t(I18nKey.BILLING$YOURE_IN));
|
||||
}
|
||||
}, [error?.status, pathname, isFetching, tosPageStatus]);
|
||||
}, [settings?.IS_NEW_USER, config.data?.APP_MODE]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Don't do any redirects when on TOS page
|
||||
// Don't allow users to use the app if it 402s
|
||||
if (!tosPageStatus && error?.status === 402 && pathname !== "/") {
|
||||
navigate("/");
|
||||
}
|
||||
}, [error?.status, pathname, 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;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { I18nextProvider, initReactI18next } from "react-i18next";
|
||||
import i18n from "i18next";
|
||||
import { vi } from "vitest";
|
||||
import { AxiosError } from "axios";
|
||||
import { AppStore, RootState, rootReducer } from "./src/store";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { ConversationProvider } from "#/context/conversation-context";
|
||||
@@ -83,3 +84,19 @@ export function renderWithProviders(
|
||||
}
|
||||
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
|
||||
}
|
||||
|
||||
export const createAxiosNotFoundErrorObject = () =>
|
||||
new AxiosError(
|
||||
"Request failed with status code 404",
|
||||
"ERR_BAD_REQUEST",
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
status: 404,
|
||||
statusText: "Not Found",
|
||||
data: { message: "Settings not found" },
|
||||
headers: {},
|
||||
// @ts-expect-error - we only need the response object for this test
|
||||
config: {},
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user