mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
feat: invisible recaptcha
This commit is contained in:
parent
b49b309885
commit
0088489c22
@ -1,6 +1,7 @@
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import { it, describe, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
import { AuthModal } from "#/components/features/waitlist/auth-modal";
|
||||
import AuthService from "#/api/auth-service/auth-service.api";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
@ -17,22 +18,33 @@ vi.mock("#/hooks/use-tracking", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock the useRecaptcha hook
|
||||
const mockGetRecaptchaResponse = vi.hoisted(() => vi.fn());
|
||||
const mockUseRecaptcha = vi.hoisted(() =>
|
||||
vi.fn(() => ({
|
||||
recaptchaLoaded: true,
|
||||
recaptchaError: false,
|
||||
widgetId: 1,
|
||||
recaptchaRef: { current: null },
|
||||
getRecaptchaResponse: mockGetRecaptchaResponse,
|
||||
resetRecaptcha: vi.fn(),
|
||||
})),
|
||||
// Minimal mock for react-google-recaptcha - just expose executeAsync via ref
|
||||
type RecaptchaHandle = {
|
||||
executeAsync: () => Promise<string | null>;
|
||||
reset: () => void;
|
||||
};
|
||||
type RecaptchaProps = { sitekey: string; size: string; onError: () => void };
|
||||
|
||||
const mockExecuteAsync = vi.hoisted(() =>
|
||||
vi.fn<RecaptchaHandle["executeAsync"]>().mockResolvedValue("mock-token"),
|
||||
);
|
||||
|
||||
vi.mock("#/hooks/use-recaptcha", () => ({
|
||||
useRecaptcha: mockUseRecaptcha,
|
||||
}));
|
||||
vi.mock("react-google-recaptcha", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: React.forwardRef<RecaptchaHandle, RecaptchaProps>((props, ref) => {
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
executeAsync: mockExecuteAsync,
|
||||
reset: vi.fn(),
|
||||
}));
|
||||
return React.createElement("div", {
|
||||
"data-testid": "recaptcha-widget",
|
||||
"data-sitekey": props.sitekey,
|
||||
"data-size": props.size,
|
||||
});
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe("AuthModal", () => {
|
||||
let verifyRecaptchaSpy: ReturnType<typeof vi.spyOn>;
|
||||
@ -40,7 +52,7 @@ describe("AuthModal", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("location", { href: "" });
|
||||
verifyRecaptchaSpy = vi.spyOn(AuthService, "verifyRecaptcha");
|
||||
mockGetRecaptchaResponse.mockReturnValue("");
|
||||
mockExecuteAsync.mockResolvedValue("mock-token");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -176,12 +188,12 @@ describe("AuthModal", () => {
|
||||
expect(verifyRecaptchaSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should block auth and show error when reCAPTCHA is not completed", async () => {
|
||||
it("should block auth and show error when reCAPTCHA execution returns no token", async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup();
|
||||
vi.stubEnv("VITE_RECAPTCHA_SITE_KEY", "test-site-key");
|
||||
const mockUrl = "https://github.com/login/oauth/authorize";
|
||||
mockGetRecaptchaResponse.mockReturnValue("");
|
||||
mockExecuteAsync.mockResolvedValue(null);
|
||||
|
||||
renderWithProviders(
|
||||
<AuthModal
|
||||
@ -198,7 +210,9 @@ describe("AuthModal", () => {
|
||||
await user.click(githubButton);
|
||||
|
||||
// Assert
|
||||
expect(window.location.href).toBe("");
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe("");
|
||||
});
|
||||
expect(screen.getByText(/AUTH\$RECAPTCHA_REQUIRED/i)).toBeInTheDocument();
|
||||
expect(verifyRecaptchaSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
@ -209,7 +223,7 @@ describe("AuthModal", () => {
|
||||
vi.stubEnv("VITE_RECAPTCHA_SITE_KEY", "test-site-key");
|
||||
const mockUrl = "https://github.com/login/oauth/authorize";
|
||||
const mockToken = "recaptcha-token-123";
|
||||
mockGetRecaptchaResponse.mockReturnValue(mockToken);
|
||||
mockExecuteAsync.mockResolvedValue(mockToken);
|
||||
verifyRecaptchaSpy.mockResolvedValue({ success: false });
|
||||
|
||||
renderWithProviders(
|
||||
@ -240,7 +254,7 @@ describe("AuthModal", () => {
|
||||
vi.stubEnv("VITE_RECAPTCHA_SITE_KEY", "test-site-key");
|
||||
const mockUrl = "https://github.com/login/oauth/authorize";
|
||||
const mockToken = "recaptcha-token-123";
|
||||
mockGetRecaptchaResponse.mockReturnValue(mockToken);
|
||||
mockExecuteAsync.mockResolvedValue(mockToken);
|
||||
verifyRecaptchaSpy.mockResolvedValue({ success: true });
|
||||
|
||||
renderWithProviders(
|
||||
@ -267,7 +281,7 @@ describe("AuthModal", () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render reCAPTCHA widget container when site key is configured", () => {
|
||||
it("should render reCAPTCHA widget when site key is configured", () => {
|
||||
// Arrange
|
||||
vi.stubEnv("VITE_RECAPTCHA_SITE_KEY", "test-site-key");
|
||||
|
||||
@ -281,10 +295,7 @@ describe("AuthModal", () => {
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(mockUseRecaptcha).toHaveBeenCalledWith({
|
||||
siteKey: "test-site-key",
|
||||
enabled: true,
|
||||
});
|
||||
expect(screen.getByTestId("recaptcha-widget")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not render reCAPTCHA widget when site key is not configured", () => {
|
||||
@ -301,8 +312,7 @@ describe("AuthModal", () => {
|
||||
);
|
||||
|
||||
// Assert
|
||||
const recaptchaContainer = document.querySelector("div[ref]");
|
||||
expect(recaptchaContainer).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("recaptcha-widget")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,280 +0,0 @@
|
||||
import { act, renderHook, waitFor, render } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useRecaptcha } from "#/hooks/use-recaptcha";
|
||||
import React from "react";
|
||||
|
||||
describe("useRecaptcha", () => {
|
||||
let mockGrecaptcha: {
|
||||
ready: (callback: () => void) => void;
|
||||
render: (
|
||||
element: HTMLElement,
|
||||
options: {
|
||||
sitekey: string;
|
||||
callback?: (token: string) => void;
|
||||
"expired-callback"?: () => void;
|
||||
"error-callback"?: () => void;
|
||||
},
|
||||
) => number;
|
||||
getResponse: (widgetId?: number) => string;
|
||||
reset: (widgetId?: number) => void;
|
||||
};
|
||||
let mockScript: HTMLScriptElement;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock grecaptcha API
|
||||
mockGrecaptcha = {
|
||||
ready: vi.fn((callback: () => void) => callback()),
|
||||
render: vi.fn(() => 1),
|
||||
getResponse: vi.fn(() => ""),
|
||||
reset: vi.fn(),
|
||||
};
|
||||
|
||||
// Mock script element - create a real script element so it can be appended to DOM
|
||||
const originalCreateElement = document.createElement.bind(document);
|
||||
mockScript = originalCreateElement("script") as HTMLScriptElement;
|
||||
|
||||
// Mock DOM methods
|
||||
vi.stubGlobal("grecaptcha", undefined);
|
||||
vi.spyOn(document, "createElement").mockImplementation(
|
||||
(tagName: string) => {
|
||||
if (tagName === "script") {
|
||||
return mockScript;
|
||||
}
|
||||
return originalCreateElement(tagName);
|
||||
},
|
||||
);
|
||||
vi.spyOn(document.head, "appendChild").mockImplementation(
|
||||
vi.fn((node: Node) => node),
|
||||
);
|
||||
vi.spyOn(document, "querySelector").mockReturnValue(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should return initial state when siteKey is undefined", () => {
|
||||
// Arrange & Act
|
||||
const { result } = renderHook(() => useRecaptcha({ siteKey: undefined }));
|
||||
|
||||
// Assert
|
||||
expect(result.current.recaptchaLoaded).toBe(false);
|
||||
expect(result.current.recaptchaError).toBe(false);
|
||||
expect(result.current.widgetId).toBe(null);
|
||||
expect(result.current.recaptchaRef.current).toBe(null);
|
||||
expect(document.createElement).not.toHaveBeenCalledWith("script");
|
||||
});
|
||||
|
||||
it("should not load script when enabled is false", () => {
|
||||
// Arrange & Act
|
||||
const { result } = renderHook(() =>
|
||||
useRecaptcha({ siteKey: "test-site-key", enabled: false }),
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result.current.recaptchaLoaded).toBe(false);
|
||||
expect(document.createElement).not.toHaveBeenCalledWith("script");
|
||||
});
|
||||
|
||||
it("should load script when siteKey is provided and grecaptcha not available", () => {
|
||||
// Arrange
|
||||
vi.stubGlobal("grecaptcha", undefined);
|
||||
|
||||
// Act
|
||||
renderHook(() => useRecaptcha({ siteKey: "test-site-key" }));
|
||||
|
||||
// Assert
|
||||
expect(document.createElement).toHaveBeenCalledWith("script");
|
||||
expect(mockScript.src).toBe(
|
||||
"https://www.google.com/recaptcha/api.js?render=explicit",
|
||||
);
|
||||
expect(mockScript.async).toBe(true);
|
||||
expect(mockScript.defer).toBe(true);
|
||||
expect(document.head.appendChild).toHaveBeenCalledWith(mockScript);
|
||||
});
|
||||
|
||||
it("should render widget when grecaptcha is already available", async () => {
|
||||
// Arrange
|
||||
vi.stubGlobal("grecaptcha", mockGrecaptcha);
|
||||
|
||||
const TestComponent = () => {
|
||||
const { recaptchaRef, recaptchaLoaded, widgetId } = useRecaptcha({
|
||||
siteKey: "test-site-key",
|
||||
});
|
||||
return <div ref={recaptchaRef} data-testid="recaptcha-container" />;
|
||||
};
|
||||
|
||||
// Act
|
||||
render(<TestComponent />);
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockGrecaptcha.render).toHaveBeenCalled();
|
||||
});
|
||||
const renderCall = vi.mocked(mockGrecaptcha.render).mock.calls[0];
|
||||
expect(renderCall[1]).toMatchObject({
|
||||
sitekey: "test-site-key",
|
||||
callback: expect.any(Function),
|
||||
"expired-callback": expect.any(Function),
|
||||
"error-callback": expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("should set error state when script fails to load", async () => {
|
||||
// Arrange
|
||||
vi.stubGlobal("grecaptcha", undefined);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useRecaptcha({ siteKey: "test-site-key" }),
|
||||
);
|
||||
|
||||
// Act - simulate script error
|
||||
act(() => {
|
||||
if (mockScript.onerror) {
|
||||
mockScript.onerror(new Event("error"));
|
||||
}
|
||||
});
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(result.current.recaptchaError).toBe(true);
|
||||
});
|
||||
expect(result.current.recaptchaLoaded).toBe(false);
|
||||
});
|
||||
|
||||
it("should return response token when widget is ready", async () => {
|
||||
// Arrange
|
||||
const mockToken = "recaptcha-token-123";
|
||||
mockGrecaptcha.getResponse = vi.fn(() => mockToken);
|
||||
vi.stubGlobal("grecaptcha", mockGrecaptcha);
|
||||
|
||||
const TestComponent = () => {
|
||||
const { recaptchaRef, getRecaptchaResponse } = useRecaptcha({
|
||||
siteKey: "test-site-key",
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<div ref={recaptchaRef} data-testid="recaptcha-container" />
|
||||
<button
|
||||
onClick={() => {
|
||||
const response = getRecaptchaResponse();
|
||||
// Store response for testing
|
||||
(window as any).testResponse = response;
|
||||
}}
|
||||
>
|
||||
Get Response
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Act
|
||||
const { getByRole } = render(<TestComponent />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGrecaptcha.render).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
getByRole("button").click();
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect((window as any).testResponse).toBe(mockToken);
|
||||
expect(mockGrecaptcha.getResponse).toHaveBeenCalledWith(1);
|
||||
delete (window as any).testResponse;
|
||||
});
|
||||
|
||||
it("should return null when widget is not ready", () => {
|
||||
// Arrange
|
||||
vi.stubGlobal("grecaptcha", undefined);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useRecaptcha({ siteKey: "test-site-key" }),
|
||||
);
|
||||
|
||||
// Act
|
||||
const response = result.current.getRecaptchaResponse();
|
||||
|
||||
// Assert
|
||||
expect(response).toBe(null);
|
||||
});
|
||||
|
||||
it("should reset widget when resetRecaptcha is called", async () => {
|
||||
// Arrange
|
||||
vi.stubGlobal("grecaptcha", mockGrecaptcha);
|
||||
|
||||
const TestComponent = () => {
|
||||
const { recaptchaRef, resetRecaptcha } = useRecaptcha({
|
||||
siteKey: "test-site-key",
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<div ref={recaptchaRef} data-testid="recaptcha-container" />
|
||||
<button onClick={resetRecaptcha}>Reset</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Act
|
||||
const { getByRole } = render(<TestComponent />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGrecaptcha.render).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
getByRole("button").click();
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(mockGrecaptcha.reset).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it("should handle widget render error gracefully", async () => {
|
||||
// Arrange
|
||||
mockGrecaptcha.render = vi.fn(() => {
|
||||
throw new Error("Render failed");
|
||||
});
|
||||
vi.stubGlobal("grecaptcha", mockGrecaptcha);
|
||||
|
||||
const TestComponent = () => {
|
||||
const { recaptchaRef, recaptchaError } = useRecaptcha({
|
||||
siteKey: "test-site-key",
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<div ref={recaptchaRef} data-testid="recaptcha-container" />
|
||||
{recaptchaError && <div data-testid="error">Error occurred</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Act
|
||||
const { getByTestId } = render(<TestComponent />);
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(getByTestId("error")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should cleanup script reference on unmount", () => {
|
||||
// Arrange
|
||||
const existingScript = document.createElement("script");
|
||||
vi.spyOn(document, "querySelector").mockReturnValue(existingScript);
|
||||
|
||||
const { unmount } = renderHook(() =>
|
||||
useRecaptcha({ siteKey: "test-site-key" }),
|
||||
);
|
||||
|
||||
// Act
|
||||
unmount();
|
||||
|
||||
// Assert
|
||||
expect(document.querySelector).toHaveBeenCalledWith(
|
||||
'script[src*="recaptcha/api.js"]',
|
||||
);
|
||||
});
|
||||
});
|
||||
14
frontend/global.d.ts
vendored
14
frontend/global.d.ts
vendored
@ -15,18 +15,4 @@ interface Window {
|
||||
company?: string;
|
||||
}) => void;
|
||||
};
|
||||
grecaptcha?: {
|
||||
ready: (callback: () => void) => void;
|
||||
render: (
|
||||
element: HTMLElement,
|
||||
options: {
|
||||
sitekey: string;
|
||||
callback?: (token: string) => void;
|
||||
"expired-callback"?: () => void;
|
||||
"error-callback"?: () => void;
|
||||
},
|
||||
) => number;
|
||||
reset: (widgetId?: number) => void;
|
||||
getResponse: (widgetId?: number) => string;
|
||||
};
|
||||
}
|
||||
|
||||
97
frontend/package-lock.json
generated
97
frontend/package-lock.json
generated
@ -33,6 +33,7 @@
|
||||
"posthog-js": "^1.309.1",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-google-recaptcha": "^3.1.0",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-i18next": "^16.5.0",
|
||||
"react-icons": "^5.5.0",
|
||||
@ -61,6 +62,7 @@
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-google-recaptcha": "^2.1.9",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
@ -192,7 +194,6 @@
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@ -732,7 +733,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@ -779,7 +779,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@ -2331,7 +2330,6 @@
|
||||
"version": "2.4.24",
|
||||
"resolved": "https://registry.npmjs.org/@heroui/system/-/system-2.4.24.tgz",
|
||||
"integrity": "sha512-9GKQgUc91otQfwmq6TLE72QKxtB341aK5NpBHS3gRoWYEuNN714Zl3OXwIZNvdXPJpsTaUo1ID1ibJU9tfgwdg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@heroui/react-utils": "2.1.14",
|
||||
"@heroui/system-rsc": "2.3.21",
|
||||
@ -2411,7 +2409,6 @@
|
||||
"version": "2.4.24",
|
||||
"resolved": "https://registry.npmjs.org/@heroui/theme/-/theme-2.4.24.tgz",
|
||||
"integrity": "sha512-lL+anmY4GGWwKyTbJ2PEBZE4talIZ3hu4yGpku9TktCVG2nC2YTwiWQFJ+Jcbf8Cf9vuLzI1sla5bz2jUqiBRA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@heroui/shared-utils": "2.1.12",
|
||||
"color": "^4.2.3",
|
||||
@ -5127,7 +5124,6 @@
|
||||
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
"@svgr/babel-preset": "8.1.0",
|
||||
@ -5588,7 +5584,6 @@
|
||||
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@ -5766,7 +5761,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
|
||||
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@ -5782,7 +5776,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@ -5793,11 +5786,20 @@
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-google-recaptcha": {
|
||||
"version": "2.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-google-recaptcha/-/react-google-recaptcha-2.1.9.tgz",
|
||||
"integrity": "sha512-nT31LrBDuoSZJN4QuwtQSF3O89FVHC4jLhM+NtKEmVF5R1e8OY0Jo4//x2Yapn2aNHguwgX5doAq8Zo+Ehd0ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-syntax-highlighter": {
|
||||
"version": "15.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
|
||||
@ -5834,7 +5836,6 @@
|
||||
"integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "7.18.0",
|
||||
@ -5892,7 +5893,6 @@
|
||||
"integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "7.18.0",
|
||||
"@typescript-eslint/types": "7.18.0",
|
||||
@ -6406,8 +6406,7 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
@ -6435,7 +6434,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@ -6972,7 +6970,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@ -7661,8 +7658,7 @@
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
"version": "1.0.8",
|
||||
@ -8380,7 +8376,6 @@
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@ -8504,7 +8499,6 @@
|
||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
@ -8585,7 +8579,6 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@ -8677,7 +8670,6 @@
|
||||
"integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"aria-query": "^5.3.2",
|
||||
"array-includes": "^3.1.8",
|
||||
@ -8773,7 +8765,6 @@
|
||||
"integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"array-includes": "^3.1.8",
|
||||
"array.prototype.findlast": "^1.2.5",
|
||||
@ -8807,7 +8798,6 @@
|
||||
"integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@ -9076,7 +9066,6 @@
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@ -9407,7 +9396,6 @@
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz",
|
||||
"integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.23.23",
|
||||
"motion-utils": "^12.23.6",
|
||||
@ -9955,6 +9943,21 @@
|
||||
"integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hoist-non-react-statics/node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
||||
@ -10075,7 +10078,6 @@
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4"
|
||||
},
|
||||
@ -10853,7 +10855,6 @@
|
||||
"integrity": "sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@acemir/cssom": "^0.9.28",
|
||||
"@asamuzakjp/dom-selector": "^6.7.6",
|
||||
@ -12555,7 +12556,6 @@
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
|
||||
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"dompurify": "3.2.7",
|
||||
"marked": "14.0.0"
|
||||
@ -12650,7 +12650,6 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@inquirer/confirm": "^5.0.0",
|
||||
"@mswjs/interceptors": "^0.40.0",
|
||||
@ -13375,7 +13374,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@ -13437,7 +13435,6 @@
|
||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@ -13634,11 +13631,23 @@
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-async-script": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-async-script/-/react-async-script-1.2.0.tgz",
|
||||
"integrity": "sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"prop-types": "^15.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-base16-styling": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.9.1.tgz",
|
||||
@ -13683,7 +13692,6 @@
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@ -13691,6 +13699,19 @@
|
||||
"react": "^19.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/react-google-recaptcha": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz",
|
||||
"integrity": "sha512-cYW2/DWas8nEKZGD7SCu9BSuVz8iOcOLHChHyi7upUuVhkpkhYG/6N3KDiTQ3XAiZ2UAZkfvYKMfAHOzBOcGEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.5.0",
|
||||
"react-async-script": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hot-toast": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
|
||||
@ -13796,7 +13817,6 @@
|
||||
"version": "7.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.11.0.tgz",
|
||||
"integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
@ -14158,7 +14178,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
|
||||
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@ -15154,7 +15173,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
|
||||
"integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
@ -15281,7 +15299,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -15583,7 +15600,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@ -15889,7 +15905,6 @@
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
|
||||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@ -16059,7 +16074,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -16072,7 +16086,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz",
|
||||
"integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.16",
|
||||
"@vitest/mocker": "4.0.16",
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
"posthog-js": "^1.309.1",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-google-recaptcha": "^3.1.0",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-i18next": "^16.5.0",
|
||||
"react-icons": "^5.5.0",
|
||||
@ -92,6 +93,7 @@
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-google-recaptcha": "^2.1.9",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ReCAPTCHA from "react-google-recaptcha";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import OpenHandsLogo from "#/assets/branding/openhands-logo.svg?react";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
@ -13,7 +14,6 @@ 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 { useRecaptcha } from "#/hooks/use-recaptcha";
|
||||
import { useVerifyRecaptcha } from "#/hooks/mutation/use-verify-recaptcha";
|
||||
|
||||
interface AuthModalProps {
|
||||
@ -32,20 +32,11 @@ export function AuthModal({
|
||||
const { t } = useTranslation();
|
||||
const { trackLoginButtonClick } = useTracking();
|
||||
const [recaptchaError, setRecaptchaError] = useState(false);
|
||||
const recaptchaRef = useRef<ReCAPTCHA>(null);
|
||||
|
||||
// Get reCAPTCHA site key from environment variable
|
||||
const recaptchaSiteKey = import.meta.env.VITE_RECAPTCHA_SITE_KEY || undefined;
|
||||
|
||||
// Initialize reCAPTCHA
|
||||
const {
|
||||
recaptchaRef,
|
||||
getRecaptchaResponse,
|
||||
recaptchaError: recaptchaLoadError,
|
||||
} = useRecaptcha({
|
||||
siteKey: recaptchaSiteKey,
|
||||
enabled: !!recaptchaSiteKey,
|
||||
});
|
||||
|
||||
// Hook for verifying reCAPTCHA with backend
|
||||
const { mutateAsync: verifyRecaptcha } = useVerifyRecaptcha();
|
||||
|
||||
@ -80,15 +71,22 @@ export function AuthModal({
|
||||
return true;
|
||||
}
|
||||
|
||||
const response = getRecaptchaResponse();
|
||||
if (!response) {
|
||||
if (!recaptchaRef.current) {
|
||||
setRecaptchaError(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
// For invisible reCAPTCHA, execute the challenge first
|
||||
try {
|
||||
const token = await recaptchaRef.current.executeAsync();
|
||||
if (!token) {
|
||||
setRecaptchaError(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify the token with the backend using the mutation hook
|
||||
const verificationResult = await verifyRecaptcha(response);
|
||||
const verificationResult = await verifyRecaptcha(token);
|
||||
console.log("verificationResult", verificationResult);
|
||||
if (!verificationResult.success) {
|
||||
setRecaptchaError(true);
|
||||
return false;
|
||||
@ -277,12 +275,14 @@ export function AuthModal({
|
||||
|
||||
{recaptchaSiteKey && (
|
||||
<div className="flex justify-center mt-2">
|
||||
<div ref={recaptchaRef} />
|
||||
{recaptchaLoadError && (
|
||||
<div className="text-xs text-muted-foreground text-center mt-1">
|
||||
{t(I18nKey.AUTH$RECAPTCHA_LOAD_ERROR)}
|
||||
</div>
|
||||
)}
|
||||
<ReCAPTCHA
|
||||
ref={recaptchaRef}
|
||||
sitekey={recaptchaSiteKey}
|
||||
size="invisible"
|
||||
onError={() => {
|
||||
setRecaptchaError(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -1,134 +0,0 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface UseRecaptchaOptions {
|
||||
siteKey: string | undefined;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
interface UseRecaptchaReturn {
|
||||
recaptchaLoaded: boolean;
|
||||
recaptchaError: boolean;
|
||||
widgetId: number | null;
|
||||
recaptchaRef: React.RefObject<HTMLDivElement | null>;
|
||||
getRecaptchaResponse: () => string | null;
|
||||
resetRecaptcha: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to load and manage Google reCAPTCHA v2
|
||||
* @param siteKey - The reCAPTCHA site key
|
||||
* @param enabled - Whether to load reCAPTCHA (default: true)
|
||||
* @returns Object with reCAPTCHA state and methods
|
||||
*/
|
||||
export function useRecaptcha({
|
||||
siteKey,
|
||||
enabled = true,
|
||||
}: UseRecaptchaOptions): UseRecaptchaReturn {
|
||||
const [recaptchaLoaded, setRecaptchaLoaded] = useState(false);
|
||||
const [recaptchaError, setRecaptchaError] = useState(false);
|
||||
const [widgetId, setWidgetId] = useState<number | null>(null);
|
||||
const recaptchaRef = useRef<HTMLDivElement>(null);
|
||||
const scriptLoadedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || !siteKey || scriptLoadedRef.current) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Check if script is already loaded
|
||||
if (window.grecaptcha) {
|
||||
setRecaptchaLoaded(true);
|
||||
scriptLoadedRef.current = true;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Load the reCAPTCHA script
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://www.google.com/recaptcha/api.js?render=explicit";
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
|
||||
script.onload = () => {
|
||||
if (window.grecaptcha) {
|
||||
window.grecaptcha.ready(() => {
|
||||
setRecaptchaLoaded(true);
|
||||
scriptLoadedRef.current = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
script.onerror = () => {
|
||||
setRecaptchaError(true);
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
|
||||
return () => {
|
||||
// Cleanup: remove script if component unmounts
|
||||
const existingScript = document.querySelector(
|
||||
'script[src*="recaptcha/api.js"]',
|
||||
);
|
||||
if (existingScript) {
|
||||
// Don't remove script as it might be used elsewhere
|
||||
// Just reset the state
|
||||
setRecaptchaLoaded(false);
|
||||
scriptLoadedRef.current = false;
|
||||
}
|
||||
};
|
||||
}, [siteKey, enabled]);
|
||||
|
||||
// Render the reCAPTCHA widget when script is loaded
|
||||
useEffect(() => {
|
||||
if (
|
||||
!recaptchaLoaded ||
|
||||
!siteKey ||
|
||||
!recaptchaRef.current ||
|
||||
widgetId !== null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.grecaptcha && recaptchaRef.current) {
|
||||
try {
|
||||
const id = window.grecaptcha.render(recaptchaRef.current, {
|
||||
sitekey: siteKey,
|
||||
callback: () => {
|
||||
// CAPTCHA completed successfully
|
||||
},
|
||||
"expired-callback": () => {
|
||||
// CAPTCHA expired
|
||||
},
|
||||
"error-callback": () => {
|
||||
// CAPTCHA error
|
||||
},
|
||||
});
|
||||
setWidgetId(id);
|
||||
} catch (error) {
|
||||
setRecaptchaError(true);
|
||||
}
|
||||
}
|
||||
}, [recaptchaLoaded, siteKey, widgetId]);
|
||||
|
||||
const getRecaptchaResponse = (): string | null => {
|
||||
if (!window.grecaptcha || widgetId === null) {
|
||||
return null;
|
||||
}
|
||||
const response = window.grecaptcha.getResponse(widgetId);
|
||||
return response || null;
|
||||
};
|
||||
|
||||
const resetRecaptcha = () => {
|
||||
if (window.grecaptcha && widgetId !== null) {
|
||||
window.grecaptcha.reset(widgetId);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
recaptchaLoaded,
|
||||
recaptchaError,
|
||||
widgetId,
|
||||
recaptchaRef,
|
||||
getRecaptchaResponse,
|
||||
resetRecaptcha,
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user