feat: require email verification for new signups (#12123)

This commit is contained in:
Hiep Le
2025-12-24 14:56:02 +07:00
committed by GitHub
parent dc99c7b62e
commit e2b2aa52cd
15 changed files with 810 additions and 33 deletions

View File

@@ -77,7 +77,7 @@ describe("AuthModal", () => {
);
// Find the terms of service section using data-testid
const termsSection = screen.getByTestId("auth-modal-terms-of-service");
const termsSection = screen.getByTestId("terms-and-privacy-notice");
expect(termsSection).toBeInTheDocument();
// Check that all text content is present in the paragraph
@@ -114,6 +114,38 @@ describe("AuthModal", () => {
expect(termsSection).toContainElement(privacyLink);
});
it("should display email verified message when emailVerified prop is true", () => {
render(
<MemoryRouter>
<AuthModal
githubAuthUrl="mock-url"
appMode="saas"
emailVerified={true}
/>
</MemoryRouter>,
);
expect(
screen.getByText("AUTH$EMAIL_VERIFIED_PLEASE_LOGIN"),
).toBeInTheDocument();
});
it("should not display email verified message when emailVerified prop is false", () => {
render(
<MemoryRouter>
<AuthModal
githubAuthUrl="mock-url"
appMode="saas"
emailVerified={false}
/>
</MemoryRouter>,
);
expect(
screen.queryByText("AUTH$EMAIL_VERIFIED_PLEASE_LOGIN"),
).not.toBeInTheDocument();
});
it("should open Terms of Service link in new tab", () => {
render(
<MemoryRouter>
@@ -142,12 +174,17 @@ describe("AuthModal", () => {
describe("Duplicate email error message", () => {
const renderAuthModalWithRouter = (initialEntries: string[]) => {
const hasDuplicatedEmail = initialEntries.includes(
"/?duplicated_email=true",
);
return render(
<MemoryRouter initialEntries={initialEntries}>
<AuthModal
githubAuthUrl="mock-url"
appMode="saas"
providersConfigured={["github"]}
hasDuplicatedEmail={hasDuplicatedEmail}
/>
</MemoryRouter>,
);

View File

@@ -0,0 +1,28 @@
import { render, screen } from "@testing-library/react";
import { it, describe, expect, vi, beforeEach } from "vitest";
import { EmailVerificationModal } from "#/components/features/waitlist/email-verification-modal";
describe("EmailVerificationModal", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should render the email verification message", () => {
// Arrange & Act
render(<EmailVerificationModal onClose={vi.fn()} />);
// Assert
expect(
screen.getByText("AUTH$PLEASE_CHECK_EMAIL_TO_VERIFY"),
).toBeInTheDocument();
});
it("should render the TermsAndPrivacyNotice component", () => {
// Arrange & Act
render(<EmailVerificationModal onClose={vi.fn()} />);
// Assert
const termsSection = screen.getByTestId("terms-and-privacy-notice");
expect(termsSection).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,48 @@
import { render, screen } from "@testing-library/react";
import { it, describe, expect } from "vitest";
import { TermsAndPrivacyNotice } from "#/components/shared/terms-and-privacy-notice";
describe("TermsAndPrivacyNotice", () => {
it("should render Terms of Service and Privacy Policy links", () => {
// Arrange & Act
render(<TermsAndPrivacyNotice />);
// Assert
const termsSection = screen.getByTestId("terms-and-privacy-notice");
expect(termsSection).toBeInTheDocument();
const tosLink = screen.getByRole("link", {
name: "COMMON$TERMS_OF_SERVICE",
});
const privacyLink = screen.getByRole("link", {
name: "COMMON$PRIVACY_POLICY",
});
expect(tosLink).toBeInTheDocument();
expect(tosLink).toHaveAttribute("href", "https://www.all-hands.dev/tos");
expect(tosLink).toHaveAttribute("target", "_blank");
expect(tosLink).toHaveAttribute("rel", "noopener noreferrer");
expect(privacyLink).toBeInTheDocument();
expect(privacyLink).toHaveAttribute(
"href",
"https://www.all-hands.dev/privacy",
);
expect(privacyLink).toHaveAttribute("target", "_blank");
expect(privacyLink).toHaveAttribute("rel", "noopener noreferrer");
});
it("should render all required text content", () => {
// Arrange & Act
render(<TermsAndPrivacyNotice />);
// Assert
const termsSection = screen.getByTestId("terms-and-privacy-notice");
expect(termsSection).toHaveTextContent(
"AUTH$BY_SIGNING_UP_YOU_AGREE_TO_OUR",
);
expect(termsSection).toHaveTextContent("COMMON$TERMS_OF_SERVICE");
expect(termsSection).toHaveTextContent("COMMON$AND");
expect(termsSection).toHaveTextContent("COMMON$PRIVACY_POLICY");
});
});

View File

@@ -0,0 +1,242 @@
import { render, screen, waitFor } from "@testing-library/react";
import { it, describe, expect, vi, beforeEach, afterEach } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRoutesStub } from "react-router";
import MainApp from "#/routes/root-layout";
import OptionService from "#/api/option-service/option-service.api";
import AuthService from "#/api/auth-service/auth-service.api";
import SettingsService from "#/api/settings-service/settings-service.api";
// Mock other hooks that are not the focus of these tests
vi.mock("#/hooks/use-github-auth-url", () => ({
useGitHubAuthUrl: () => "https://github.com/oauth/authorize",
}));
vi.mock("#/hooks/use-is-on-tos-page", () => ({
useIsOnTosPage: () => false,
}));
vi.mock("#/hooks/use-auto-login", () => ({
useAutoLogin: () => {},
}));
vi.mock("#/hooks/use-auth-callback", () => ({
useAuthCallback: () => {},
}));
vi.mock("#/hooks/use-migrate-user-consent", () => ({
useMigrateUserConsent: () => ({
migrateUserConsent: vi.fn(),
}),
}));
vi.mock("#/hooks/use-reo-tracking", () => ({
useReoTracking: () => {},
}));
vi.mock("#/hooks/use-sync-posthog-consent", () => ({
useSyncPostHogConsent: () => {},
}));
vi.mock("#/utils/custom-toast-handlers", () => ({
displaySuccessToast: vi.fn(),
}));
const RouterStub = createRoutesStub([
{
Component: MainApp,
path: "/",
children: [
{
Component: () => <div data-testid="outlet-content">Content</div>,
path: "/",
},
],
},
]);
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
describe("MainApp - Email Verification Flow", () => {
beforeEach(() => {
vi.clearAllMocks();
// Default mocks for services
vi.spyOn(OptionService, "getConfig").mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "test-client-id",
POSTHOG_CLIENT_KEY: "test-posthog-key",
PROVIDERS_CONFIGURED: ["github"],
AUTH_URL: "https://auth.example.com",
FEATURE_FLAGS: {
ENABLE_BILLING: false,
HIDE_LLM_SETTINGS: false,
ENABLE_JIRA: false,
ENABLE_JIRA_DC: false,
ENABLE_LINEAR: false,
},
});
vi.spyOn(AuthService, "authenticate").mockResolvedValue(true);
vi.spyOn(SettingsService, "getSettings").mockResolvedValue({
language: "en",
user_consents_to_analytics: true,
llm_model: "",
llm_base_url: "",
agent: "",
llm_api_key: null,
llm_api_key_set: false,
search_api_key_set: false,
confirmation_mode: false,
security_analyzer: null,
remote_runtime_resource_factor: null,
provider_tokens_set: {},
enable_default_condenser: false,
condenser_max_size: null,
enable_sound_notifications: false,
enable_proactive_conversation_starters: false,
enable_solvability_analysis: false,
max_budget_per_task: null,
});
// Mock localStorage
vi.stubGlobal("localStorage", {
getItem: vi.fn(() => null),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
});
});
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
it("should display EmailVerificationModal when email_verification_required=true is in query params", async () => {
// Arrange & Act
render(
<RouterStub initialEntries={["/?email_verification_required=true"]} />,
{ wrapper: createWrapper() },
);
// Assert
await waitFor(() => {
expect(
screen.getByText("AUTH$PLEASE_CHECK_EMAIL_TO_VERIFY"),
).toBeInTheDocument();
});
});
it("should set emailVerified state and pass to AuthModal when email_verified=true is in query params", async () => {
// Arrange
// Mock a 401 error to simulate unauthenticated user
const axiosError = {
response: { status: 401 },
isAxiosError: true,
};
vi.spyOn(AuthService, "authenticate").mockRejectedValue(axiosError);
// Act
render(<RouterStub initialEntries={["/?email_verified=true"]} />, {
wrapper: createWrapper(),
});
// Assert - Wait for AuthModal to render (since user is not authenticated)
await waitFor(() => {
expect(
screen.getByText("AUTH$EMAIL_VERIFIED_PLEASE_LOGIN"),
).toBeInTheDocument();
});
});
it("should handle both email_verification_required and email_verified params together", async () => {
// Arrange & Act
render(
<RouterStub
initialEntries={[
"/?email_verification_required=true&email_verified=true",
]}
/>,
{ wrapper: createWrapper() },
);
// Assert - EmailVerificationModal should take precedence
await waitFor(() => {
expect(
screen.getByText("AUTH$PLEASE_CHECK_EMAIL_TO_VERIFY"),
).toBeInTheDocument();
});
});
it("should remove query parameters from URL after processing", async () => {
// Arrange & Act
const { container } = render(
<RouterStub initialEntries={["/?email_verification_required=true"]} />,
{ wrapper: createWrapper() },
);
// Assert - Wait for the modal to appear (which indicates processing happened)
await waitFor(() => {
expect(
screen.getByText("AUTH$PLEASE_CHECK_EMAIL_TO_VERIFY"),
).toBeInTheDocument();
});
// Verify that the query parameter was processed by checking the modal appeared
// The hook removes the parameter from the URL, so we verify the behavior indirectly
expect(container).toBeInTheDocument();
});
it("should not display EmailVerificationModal when email_verification_required is not in query params", async () => {
// Arrange - No query params set
// Act
render(<RouterStub />, { wrapper: createWrapper() });
// Assert
await waitFor(() => {
expect(
screen.queryByText("AUTH$PLEASE_CHECK_EMAIL_TO_VERIFY"),
).not.toBeInTheDocument();
});
});
it("should not display email verified message when email_verified is not in query params", async () => {
// Arrange
// Mock a 401 error to simulate unauthenticated user
const axiosError = {
response: { status: 401 },
isAxiosError: true,
};
vi.spyOn(AuthService, "authenticate").mockRejectedValue(axiosError);
// Act
render(<RouterStub />, { wrapper: createWrapper() });
// Assert - AuthModal should render but without email verified message
await waitFor(() => {
const authModal = screen.queryByText(
"AUTH$SIGN_IN_WITH_IDENTITY_PROVIDER",
);
if (authModal) {
expect(
screen.queryByText("AUTH$EMAIL_VERIFIED_PLEASE_LOGIN"),
).not.toBeInTheDocument();
}
});
});
});

View File

@@ -1,6 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { useSearchParams } from "react-router";
import { I18nKey } from "#/i18n/declaration";
import OpenHandsLogo from "#/assets/branding/openhands-logo.svg?react";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
@@ -14,12 +13,15 @@ import { useAuthUrl } from "#/hooks/use-auth-url";
import { GetConfigResponse } from "#/api/option-service/option.types";
import { Provider } from "#/types/settings";
import { useTracking } from "#/hooks/use-tracking";
import { TermsAndPrivacyNotice } from "#/components/shared/terms-and-privacy-notice";
interface AuthModalProps {
githubAuthUrl: string | null;
appMode?: GetConfigResponse["APP_MODE"] | null;
authUrl?: GetConfigResponse["AUTH_URL"];
providersConfigured?: Provider[];
emailVerified?: boolean;
hasDuplicatedEmail?: boolean;
}
export function AuthModal({
@@ -27,11 +29,11 @@ export function AuthModal({
appMode,
authUrl,
providersConfigured,
emailVerified = false,
hasDuplicatedEmail = false,
}: AuthModalProps) {
const { t } = useTranslation();
const { trackLoginButtonClick } = useTracking();
const [searchParams] = useSearchParams();
const hasDuplicatedEmail = searchParams.get("duplicated_email") === "true";
const gitlabAuthUrl = useAuthUrl({
appMode: appMode || null,
@@ -126,6 +128,13 @@ export function AuthModal({
<ModalBackdrop>
<ModalBody className="border border-tertiary">
<OpenHandsLogo width={68} height={46} />
{emailVerified && (
<div className="flex flex-col gap-2 w-full items-center text-center">
<p className="text-sm text-muted-foreground">
{t(I18nKey.AUTH$EMAIL_VERIFIED_PLEASE_LOGIN)}
</p>
</div>
)}
{hasDuplicatedEmail && (
<div className="text-center text-danger text-sm mt-2 mb-2">
{t(I18nKey.AUTH$DUPLICATE_EMAIL_ERROR)}
@@ -206,30 +215,7 @@ export function AuthModal({
)}
</div>
<p
className="mt-4 text-xs text-center text-muted-foreground"
data-testid="auth-modal-terms-of-service"
>
{t(I18nKey.AUTH$BY_SIGNING_UP_YOU_AGREE_TO_OUR)}{" "}
<a
href="https://www.all-hands.dev/tos"
target="_blank"
className="underline hover:text-primary"
rel="noopener noreferrer"
>
{t(I18nKey.COMMON$TERMS_OF_SERVICE)}
</a>{" "}
{t(I18nKey.COMMON$AND)}{" "}
<a
href="https://www.all-hands.dev/privacy"
target="_blank"
className="underline hover:text-primary"
rel="noopener noreferrer"
>
{t(I18nKey.COMMON$PRIVACY_POLICY)}
</a>
.
</p>
<TermsAndPrivacyNotice />
</ModalBody>
</ModalBackdrop>
);

View File

@@ -0,0 +1,31 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import OpenHandsLogo from "#/assets/branding/openhands-logo.svg?react";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
import { TermsAndPrivacyNotice } from "#/components/shared/terms-and-privacy-notice";
interface EmailVerificationModalProps {
onClose: () => void;
}
export function EmailVerificationModal({
onClose,
}: EmailVerificationModalProps) {
const { t } = useTranslation();
return (
<ModalBackdrop onClose={onClose}>
<ModalBody className="border border-tertiary">
<OpenHandsLogo 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.AUTH$PLEASE_CHECK_EMAIL_TO_VERIFY)}
</h1>
</div>
<TermsAndPrivacyNotice />
</ModalBody>
</ModalBackdrop>
);
}

View File

@@ -0,0 +1,37 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
interface TermsAndPrivacyNoticeProps {
className?: string;
}
export function TermsAndPrivacyNotice({
className = "mt-4 text-xs text-center text-muted-foreground",
}: TermsAndPrivacyNoticeProps) {
const { t } = useTranslation();
return (
<p className={className} data-testid="terms-and-privacy-notice">
{t(I18nKey.AUTH$BY_SIGNING_UP_YOU_AGREE_TO_OUR)}{" "}
<a
href="https://www.all-hands.dev/tos"
target="_blank"
className="underline hover:text-primary"
rel="noopener noreferrer"
>
{t(I18nKey.COMMON$TERMS_OF_SERVICE)}
</a>{" "}
{t(I18nKey.COMMON$AND)}{" "}
<a
href="https://www.all-hands.dev/privacy"
target="_blank"
className="underline hover:text-primary"
rel="noopener noreferrer"
>
{t(I18nKey.COMMON$PRIVACY_POLICY)}
</a>
.
</p>
);
}

View File

@@ -0,0 +1,63 @@
import React from "react";
import { useSearchParams } from "react-router";
/**
* Hook to handle email verification logic from URL query parameters.
* Manages the email verification modal state and email verified state
* based on query parameters in the URL.
*
* @returns An object containing:
* - emailVerificationModalOpen: boolean state for modal visibility
* - setEmailVerificationModalOpen: function to control modal visibility
* - emailVerified: boolean state for email verification status
* - setEmailVerified: function to control email verification status
* - hasDuplicatedEmail: boolean state for duplicate email error status
*/
export function useEmailVerification() {
const [searchParams, setSearchParams] = useSearchParams();
const [emailVerificationModalOpen, setEmailVerificationModalOpen] =
React.useState(false);
const [emailVerified, setEmailVerified] = React.useState(false);
const [hasDuplicatedEmail, setHasDuplicatedEmail] = React.useState(false);
// Check for email verification query parameters
React.useEffect(() => {
const emailVerificationRequired = searchParams.get(
"email_verification_required",
);
const emailVerifiedParam = searchParams.get("email_verified");
const duplicatedEmailParam = searchParams.get("duplicated_email");
let shouldUpdate = false;
if (emailVerificationRequired === "true") {
setEmailVerificationModalOpen(true);
searchParams.delete("email_verification_required");
shouldUpdate = true;
}
if (emailVerifiedParam === "true") {
setEmailVerified(true);
searchParams.delete("email_verified");
shouldUpdate = true;
}
if (duplicatedEmailParam === "true") {
setHasDuplicatedEmail(true);
searchParams.delete("duplicated_email");
shouldUpdate = true;
}
// Clean up the URL by removing parameters if any were found
if (shouldUpdate) {
setSearchParams(searchParams, { replace: true });
}
}, [searchParams, setSearchParams]);
return {
emailVerificationModalOpen,
setEmailVerificationModalOpen,
emailVerified,
setEmailVerified,
hasDuplicatedEmail,
};
}

View File

@@ -730,6 +730,8 @@ export enum I18nKey {
MICROAGENT_MANAGEMENT$USE_MICROAGENTS = "MICROAGENT_MANAGEMENT$USE_MICROAGENTS",
AUTH$BY_SIGNING_UP_YOU_AGREE_TO_OUR = "AUTH$BY_SIGNING_UP_YOU_AGREE_TO_OUR",
AUTH$NO_PROVIDERS_CONFIGURED = "AUTH$NO_PROVIDERS_CONFIGURED",
AUTH$PLEASE_CHECK_EMAIL_TO_VERIFY = "AUTH$PLEASE_CHECK_EMAIL_TO_VERIFY",
AUTH$EMAIL_VERIFIED_PLEASE_LOGIN = "AUTH$EMAIL_VERIFIED_PLEASE_LOGIN",
AUTH$DUPLICATE_EMAIL_ERROR = "AUTH$DUPLICATE_EMAIL_ERROR",
COMMON$TERMS_OF_SERVICE = "COMMON$TERMS_OF_SERVICE",
COMMON$AND = "COMMON$AND",

View File

@@ -11679,6 +11679,38 @@
"de": "Mindestens ein Identitätsanbieter muss konfiguriert werden (z.B. GitHub)",
"uk": "Принаймні один постачальник ідентифікації має бути налаштований (наприклад, GitHub)"
},
"AUTH$PLEASE_CHECK_EMAIL_TO_VERIFY": {
"en": "Please check your email to verify your account.",
"ja": "アカウントを確認するためにメールを確認してください。",
"zh-CN": "请检查您的电子邮件以验证您的账户。",
"zh-TW": "請檢查您的電子郵件以驗證您的帳戶。",
"ko-KR": "계정을 확인하려면 이메일을 확인하세요.",
"no": "Vennligst sjekk e-posten din for å bekrefte kontoen din.",
"it": "Controlla la tua email per verificare il tuo account.",
"pt": "Por favor, verifique seu e-mail para verificar sua conta.",
"es": "Por favor, verifica tu correo electrónico para verificar tu cuenta.",
"ar": "يرجى التحقق من بريدك الإلكتروني للتحقق من حسابك.",
"fr": "Veuillez vérifier votre e-mail pour vérifier votre compte.",
"tr": "Hesabınızı doğrulamak için lütfen e-postanızı kontrol edin.",
"de": "Bitte überprüfen Sie Ihre E-Mail, um Ihr Konto zu verifizieren.",
"uk": "Будь ласка, перевірте свою електронну пошту, щоб підтвердити свій обліковий запис."
},
"AUTH$EMAIL_VERIFIED_PLEASE_LOGIN": {
"en": "Your email has been verified. Please login below.",
"ja": "メールアドレスが確認されました。下記からログインしてください。",
"zh-CN": "您的电子邮件已验证。请在下方登录。",
"zh-TW": "您的電子郵件已驗證。請在下方登錄。",
"ko-KR": "이메일이 확인되었습니다. 아래에서 로그인하세요.",
"no": "E-posten din er bekreftet. Vennligst logg inn nedenfor.",
"it": "La tua email è stata verificata. Effettua il login qui sotto.",
"pt": "Seu e-mail foi verificado. Por favor, faça login abaixo.",
"es": "Tu correo electrónico ha sido verificado. Por favor, inicia sesión a continuación.",
"ar": "تم التحقق من بريدك الإلكتروني. يرجى تسجيل الدخول أدناه.",
"fr": "Votre e-mail a été vérifié. Veuillez vous connecter ci-dessous.",
"tr": "E-postanız doğrulandı. Lütfen aşağıdan giriş yapın.",
"de": "Ihre E-Mail wurde verifiziert. Bitte melden Sie sich unten an.",
"uk": "Вашу електронну пошту підтверджено. Будь ласка, увійдіть нижче."
},
"AUTH$DUPLICATE_EMAIL_ERROR": {
"en": "Your account is unable to be created. Please use a different login or try again.",
"ja": "アカウントを作成できません。別のログインを使用するか、もう一度お試しください。",

View File

@@ -15,6 +15,7 @@ import { useConfig } from "#/hooks/query/use-config";
import { Sidebar } from "#/components/features/sidebar/sidebar";
import { AuthModal } from "#/components/features/waitlist/auth-modal";
import { ReauthModal } from "#/components/features/waitlist/reauth-modal";
import { EmailVerificationModal } from "#/components/features/waitlist/email-verification-modal";
import { AnalyticsConsentFormModal } from "#/components/features/analytics/analytics-consent-form-modal";
import { useSettings } from "#/hooks/query/use-settings";
import { useMigrateUserConsent } from "#/hooks/use-migrate-user-consent";
@@ -26,6 +27,7 @@ import { useAutoLogin } from "#/hooks/use-auto-login";
import { useAuthCallback } from "#/hooks/use-auth-callback";
import { useReoTracking } from "#/hooks/use-reo-tracking";
import { useSyncPostHogConsent } from "#/hooks/use-sync-posthog-consent";
import { useEmailVerification } from "#/hooks/use-email-verification";
import { LOCAL_STORAGE_KEYS } from "#/utils/local-storage";
import { EmailVerificationGuard } from "#/components/features/guards/email-verification-guard";
import { MaintenanceBanner } from "#/components/features/maintenance/maintenance-banner";
@@ -91,6 +93,12 @@ export default function MainApp() {
const effectiveGitHubAuthUrl = isOnTosPage ? null : gitHubAuthUrl;
const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(false);
const {
emailVerificationModalOpen,
setEmailVerificationModalOpen,
emailVerified,
hasDuplicatedEmail,
} = useEmailVerification();
// Auto-login if login method is stored in local storage
useAutoLogin();
@@ -236,9 +244,18 @@ export default function MainApp() {
appMode={config.data?.APP_MODE}
providersConfigured={config.data?.PROVIDERS_CONFIGURED}
authUrl={config.data?.AUTH_URL}
emailVerified={emailVerified}
hasDuplicatedEmail={hasDuplicatedEmail}
/>
)}
{renderReAuthModal && <ReauthModal />}
{emailVerificationModalOpen && (
<EmailVerificationModal
onClose={() => {
setEmailVerificationModalOpen(false);
}}
/>
)}
{config.data?.APP_MODE === "oss" && consentFormIsOpen && (
<AnalyticsConsentFormModal
onClose={() => {