next-recaptcha-v3 1.5.1-canary.0 → 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,18 +18,20 @@ Straightforward solution for using ReCaptcha in your [Next.js](https://nextjs.or
18
18
  ## Install
19
19
 
20
20
  ```ssh
21
- yarn add next-recaptcha-v3
21
+ npm i next-recaptcha-v3
22
22
  ```
23
23
 
24
- or
24
+ ```ssh
25
+ pnpm i next-recaptcha-v3
26
+ ```
25
27
 
26
28
  ```ssh
27
- npm install next-recaptcha-v3 --save
29
+ yarn add next-recaptcha-v3
28
30
  ```
29
31
 
30
32
  ## Pure ESM package
31
33
 
32
- This package is now [pure ESM](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c). It cannot be `require()`'d from CommonJS.
34
+ This package is [pure ESM](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c). It cannot be `require()`'d from CommonJS.
33
35
 
34
36
  ## Generate reCAPTCHA Key
35
37
 
@@ -85,9 +87,11 @@ const {
85
87
  reCaptchaKey,
86
88
  /** Global ReCaptcha object */
87
89
  grecaptcha,
88
- /** Is ReCaptcha script loaded */
89
- loaded,
90
- /** Is ReCaptcha script failed to load */
90
+ /** If `true`, ReCaptcha script has been loaded */
91
+ isLoaded,
92
+ /** If `true`, an error occurred while loading ReCaptcha script */
93
+ isError,
94
+ /** Error received while loading ReCaptcha script */
91
95
  error,
92
96
  /** Other hook props */
93
97
  ...otherProps
@@ -195,18 +199,18 @@ import { validateToken } from "./utils";
195
199
 
196
200
  interface MyPageProps extends WithReCaptchaProps {}
197
201
 
198
- const MyPage: React.FC<MyPageProps> = ({ loaded, executeRecaptcha }) => {
202
+ const MyPage: React.FC<MyPageProps> = ({ isLoaded, executeRecaptcha }) => {
199
203
  const [token, setToken] = useState<string>(null);
200
204
 
201
205
  useEffect(() => {
202
- if (loaded) {
206
+ if (isLoaded) {
203
207
  const generateToken = async () => {
204
208
  const newToken = await executeRecaptcha("page_view");
205
209
  setToken(newToken);
206
210
  };
207
211
  generateToken();
208
212
  }
209
- }, [loaded, executeRecaptcha]);
213
+ }, [isLoaded, executeRecaptcha]);
210
214
 
211
215
  useEffect(() => {
212
216
  if (token) {
@@ -1,4 +1,4 @@
1
- interface ReCaptchaProps {
1
+ export interface ReCaptchaProps {
2
2
  onValidate: (token: string) => void;
3
3
  action: string;
4
4
  validate?: boolean;
@@ -8,6 +8,4 @@ interface ReCaptchaProps {
8
8
  * @example
9
9
  * <ReCaptcha action='form_submit' onValidate={handleToken} />
10
10
  */
11
- declare const ReCaptcha: React.FC<ReCaptchaProps>;
12
- export { ReCaptcha };
13
- export type { ReCaptchaProps };
11
+ export declare const ReCaptcha: React.FC<ReCaptchaProps>;
package/lib/ReCaptcha.js CHANGED
@@ -7,9 +7,9 @@ import { useReCaptcha } from './useReCaptcha.js';
7
7
  * <ReCaptcha action='form_submit' onValidate={handleToken} />
8
8
  */
9
9
  const ReCaptcha = ({ action, onValidate, validate = true, reCaptchaKey, }) => {
10
- const { loaded, executeRecaptcha } = useReCaptcha(reCaptchaKey);
10
+ const { isLoaded, executeRecaptcha } = useReCaptcha(reCaptchaKey);
11
11
  useEffect(() => {
12
- if (!validate || !loaded)
12
+ if (!validate || !isLoaded)
13
13
  return;
14
14
  if (typeof onValidate !== "function")
15
15
  return;
@@ -18,7 +18,7 @@ const ReCaptcha = ({ action, onValidate, validate = true, reCaptchaKey, }) => {
18
18
  onValidate(token);
19
19
  };
20
20
  handleExecuteRecaptcha();
21
- }, [action, onValidate, validate, loaded, executeRecaptcha]);
21
+ }, [action, onValidate, validate, isLoaded, executeRecaptcha]);
22
22
  return null;
23
23
  };
24
24
 
@@ -1,26 +1,32 @@
1
1
  import React from "react";
2
2
  import { ScriptProps } from "next/script.js";
3
- import type { IReCaptcha } from "./recaptcha.types.js";
4
- interface ReCaptchaContextProps {
3
+ type ReCaptchaConfigProps = {
5
4
  /** reCAPTCHA_site_key */
6
5
  readonly reCaptchaKey: string | null;
7
- /** Global ReCaptcha object */
8
- readonly grecaptcha: IReCaptcha | null;
9
- /** Is ReCaptcha script loaded */
10
- readonly loaded: boolean;
11
- /** Is ReCaptcha failed to load */
12
- readonly error: boolean;
13
- }
14
- declare const ReCaptchaContext: React.Context<ReCaptchaContextProps>;
15
- declare const useReCaptchaContext: () => ReCaptchaContextProps;
16
- interface ReCaptchaProviderProps extends Partial<Omit<ScriptProps, "onLoad">> {
6
+ /** Language code */
7
+ readonly language: string | null;
8
+ /** Use ReCaptcha Enterprise */
9
+ readonly useEnterprise: boolean;
10
+ /** Whether to use recaptcha.net for loading the script */
11
+ readonly useRecaptchaNet: boolean;
12
+ };
13
+ type ReCaptchaStateProps = {
14
+ /** If `true`, ReCaptcha script has been loaded */
15
+ readonly isLoaded: boolean;
16
+ /** If `true`, an error occurred while loading ReCaptcha script */
17
+ readonly isError: boolean;
18
+ /** Error received while loading ReCaptcha script */
19
+ readonly error: Error | null;
20
+ };
21
+ export type ReCaptchaContextProps = ReCaptchaConfigProps & ReCaptchaStateProps;
22
+ export declare const ReCaptchaContext: React.Context<ReCaptchaContextProps>;
23
+ export declare const useReCaptchaContext: () => ReCaptchaContextProps;
24
+ export interface ReCaptchaProviderProps extends Partial<ScriptProps> {
17
25
  reCaptchaKey?: string;
18
- language?: string;
26
+ language?: string | null;
19
27
  useRecaptchaNet?: boolean;
20
28
  useEnterprise?: boolean;
21
29
  children?: React.ReactNode;
22
- onLoad?: (grecaptcha: IReCaptcha, e: any) => void;
23
30
  }
24
- declare const ReCaptchaProvider: React.FC<ReCaptchaProviderProps>;
25
- export { ReCaptchaContext, useReCaptchaContext, ReCaptchaProvider };
26
- export type { ReCaptchaContextProps, ReCaptchaProviderProps };
31
+ export declare const ReCaptchaProvider: React.FC<ReCaptchaProviderProps>;
32
+ export {};
@@ -1,61 +1,51 @@
1
1
  "use client";
2
- import React, { createContext, useContext, useDebugValue, useState, useRef, useEffect, useCallback, useMemo } from 'react';
2
+ import React, { createContext, useContext, useState, useCallback, useMemo } from 'react';
3
3
  import Script from 'next/script.js';
4
- import { getRecaptchaScriptSrc } from './utils.js';
4
+ import { RECAPTCHA_LOADED_EVENT, getRecaptchaScriptSrc } from './utils.js';
5
5
 
6
6
  const ReCaptchaContext = createContext({
7
7
  reCaptchaKey: null,
8
- grecaptcha: null,
9
- loaded: false,
10
- error: false,
8
+ language: null,
9
+ useEnterprise: false,
10
+ useRecaptchaNet: false,
11
+ isLoaded: false,
12
+ isError: false,
13
+ error: null,
11
14
  });
12
- const useReCaptchaContext = () => {
13
- const values = useContext(ReCaptchaContext);
14
- useDebugValue(`grecaptcha available: ${values?.loaded ? "Yes" : "No"}`);
15
- useDebugValue(`ReCaptcha Script: ${values?.loaded ? "Loaded" : "Not Loaded"}`);
16
- useDebugValue(`Failed to load Script: ${values?.error ? "Yes" : "No"}`);
17
- return values;
18
- };
19
- const ReCaptchaProvider = ({ reCaptchaKey: passedReCaptchaKey, useEnterprise = false, useRecaptchaNet = false, language, children, id = "google-recaptcha-v3", strategy = "afterInteractive", src: passedSrc, onLoad: passedOnLoad, onError: passedOnError, ...props }) => {
20
- const [grecaptcha, setGreCaptcha] = useState(null);
21
- const [loaded, setLoaded] = useState(false);
22
- const [error, setError] = useState(false);
15
+ const useReCaptchaContext = () => useContext(ReCaptchaContext);
16
+ const ReCaptchaProvider = ({ reCaptchaKey: passedReCaptchaKey, useEnterprise = false, useRecaptchaNet = false, language = null, children, strategy = "afterInteractive", src: passedSrc, onReady: passedOnReady, onError: passedOnError, ...props }) => {
17
+ const [isLoaded, setIsLoaded] = useState(false);
18
+ const [error, setError] = useState(null);
19
+ const isError = !!error;
23
20
  const reCaptchaKey = passedReCaptchaKey || process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY || null;
24
21
  const src = passedSrc ||
25
22
  getRecaptchaScriptSrc({ reCaptchaKey, language, useRecaptchaNet, useEnterprise }) ||
26
23
  null;
27
- // Reset state when script src is changed
28
- const mounted = useRef(false);
29
- useEffect(() => {
30
- if (mounted.current) {
31
- setLoaded(false);
32
- setError(false);
33
- }
34
- mounted.current = true;
35
- }, [src]);
36
24
  // Handle script load
37
- const onLoad = useCallback((e) => {
38
- const grecaptcha = useEnterprise ? window?.grecaptcha?.enterprise : window?.grecaptcha;
39
- if (grecaptcha) {
40
- grecaptcha.ready(() => {
41
- setGreCaptcha(grecaptcha);
42
- setLoaded(true);
43
- passedOnLoad?.(grecaptcha, e);
44
- });
45
- }
46
- }, [passedOnLoad, useEnterprise]);
47
- // Run 'onLoad' function once just in case if grecaptcha is already globally available in window
48
- useEffect(() => onLoad(), [onLoad]);
25
+ const onReady = useCallback(() => {
26
+ setError(null);
27
+ setIsLoaded(true);
28
+ window.dispatchEvent(new Event(RECAPTCHA_LOADED_EVENT));
29
+ passedOnReady?.();
30
+ }, [passedOnReady]);
49
31
  // Handle script error
50
32
  const onError = useCallback((e) => {
51
- setError(true);
33
+ setError(e);
52
34
  passedOnError?.(e);
53
35
  }, [passedOnError]);
54
36
  // Prevent unnecessary rerenders
55
- const value = useMemo(() => ({ reCaptchaKey, grecaptcha, loaded, error }), [reCaptchaKey, grecaptcha, loaded, error]);
37
+ const value = useMemo(() => ({
38
+ reCaptchaKey,
39
+ language,
40
+ useEnterprise,
41
+ useRecaptchaNet,
42
+ isLoaded: isLoaded,
43
+ isError,
44
+ error,
45
+ }), [reCaptchaKey, language, useEnterprise, useRecaptchaNet, isLoaded, isError, error]);
56
46
  return (React.createElement(ReCaptchaContext.Provider, { value: value },
57
47
  children,
58
- React.createElement(Script, { id: id, src: src, strategy: strategy, onLoad: onLoad, onError: onError, ...props })));
48
+ React.createElement(Script, { src: src, strategy: strategy, onReady: onReady, onError: onError, ...props })));
59
49
  };
60
50
 
61
51
  export { ReCaptchaContext, ReCaptchaProvider, useReCaptchaContext };
@@ -1,10 +1,30 @@
1
1
  import type { ReCaptchaContextProps } from "./ReCaptchaProvider.js";
2
+ import type { IReCaptcha } from "./recaptcha.types.js";
2
3
  export interface useReCaptchaProps extends ReCaptchaContextProps {
4
+ /** reCAPTCHA instance */
5
+ grecaptcha: IReCaptcha | null;
6
+ /**
7
+ * Executes the reCAPTCHA verification process for a given action.
8
+ * Actions may only contain alphanumeric characters and slashes, and must not be user-specific.
9
+ */
3
10
  executeRecaptcha: (action: string) => Promise<string>;
4
11
  }
5
- /** React Hook to generate ReCaptcha token
12
+ /**
13
+ * Custom hook to use Google reCAPTCHA v3.
14
+ *
15
+ * @param [reCaptchaKey] - Optional reCAPTCHA site key. If not provided, it will use the key from the context.
16
+ * @returns An object containing the reCAPTCHA context, grecaptcha instance, reCaptchaKey, and executeRecaptcha function.
17
+ *
6
18
  * @example
7
- * const { executeRecaptcha } = useReCaptcha()
19
+ * const { executeRecaptcha } = useReCaptcha();
20
+ *
21
+ * const handleSubmit = async () => {
22
+ * try {
23
+ * const token = await executeRecaptcha('your_action');
24
+ * // Use the token for verification
25
+ * } catch (error) {
26
+ * console.error('ReCAPTCHA error:', error);
27
+ * }
28
+ * };
8
29
  */
9
- declare const useReCaptcha: (reCaptchaKey?: string) => useReCaptchaProps;
10
- export { useReCaptcha };
30
+ export declare const useReCaptcha: (reCaptchaKey?: string) => useReCaptchaProps;
@@ -1,31 +1,53 @@
1
1
  "use client";
2
- import { useRef, useCallback } from 'react';
2
+ import { useCallback } from 'react';
3
3
  import { useReCaptchaContext } from './ReCaptchaProvider.js';
4
- import { useIsomorphicLayoutEffect } from './utils.js';
4
+ import { useGrecaptcha, getGrecaptcha } from './utils.js';
5
5
 
6
- /** React Hook to generate ReCaptcha token
6
+ /**
7
+ * Custom hook to use Google reCAPTCHA v3.
8
+ *
9
+ * @param [reCaptchaKey] - Optional reCAPTCHA site key. If not provided, it will use the key from the context.
10
+ * @returns An object containing the reCAPTCHA context, grecaptcha instance, reCaptchaKey, and executeRecaptcha function.
11
+ *
7
12
  * @example
8
- * const { executeRecaptcha } = useReCaptcha()
13
+ * const { executeRecaptcha } = useReCaptcha();
14
+ *
15
+ * const handleSubmit = async () => {
16
+ * try {
17
+ * const token = await executeRecaptcha('your_action');
18
+ * // Use the token for verification
19
+ * } catch (error) {
20
+ * console.error('ReCAPTCHA error:', error);
21
+ * }
22
+ * };
9
23
  */
10
24
  const useReCaptcha = (reCaptchaKey) => {
11
- const { grecaptcha, loaded, reCaptchaKey: contextReCaptchaKey, ...contextProps } = useReCaptchaContext();
12
- const siteKey = reCaptchaKey || contextReCaptchaKey;
13
- // Create a ref that stores 'grecaptcha.execute' method to prevent rerenders
14
- const executeCaptchaRef = useRef(grecaptcha?.execute);
15
- useIsomorphicLayoutEffect(() => {
16
- executeCaptchaRef.current = grecaptcha?.execute;
17
- }, [loaded, grecaptcha?.execute]);
25
+ const context = useReCaptchaContext();
26
+ const { useEnterprise } = context;
27
+ const grecaptcha = useGrecaptcha(useEnterprise);
28
+ const siteKey = reCaptchaKey || context.reCaptchaKey;
18
29
  const executeRecaptcha = useCallback(async (action) => {
19
- if (typeof executeCaptchaRef.current !== "function") {
20
- throw new Error("Recaptcha has not been loaded");
21
- }
22
30
  if (!siteKey) {
23
31
  throw new Error("ReCaptcha sitekey is not defined");
24
32
  }
25
- const result = await executeCaptchaRef.current(siteKey, { action });
33
+ const grecaptcha = getGrecaptcha(useEnterprise);
34
+ if (typeof grecaptcha?.execute !== "function") {
35
+ throw new Error("Recaptcha has not been loaded");
36
+ }
37
+ if (typeof grecaptcha.ready === "function") {
38
+ await new Promise((resolve) => {
39
+ grecaptcha.ready(resolve);
40
+ });
41
+ }
42
+ const result = await grecaptcha.execute(siteKey, { action });
26
43
  return result;
27
- }, [siteKey]);
28
- return { ...contextProps, grecaptcha, loaded, reCaptchaKey: siteKey, executeRecaptcha };
44
+ }, [useEnterprise, siteKey]);
45
+ return {
46
+ ...context,
47
+ grecaptcha,
48
+ reCaptchaKey: siteKey,
49
+ executeRecaptcha,
50
+ };
29
51
  };
30
52
 
31
53
  export { useReCaptcha };
package/lib/utils.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { useLayoutEffect } from "react";
1
+ import { IReCaptcha } from "./recaptcha.types.js";
2
+ export declare const RECAPTCHA_LOADED_EVENT = "recaptcha_loaded";
2
3
  /**
3
4
  * Function to generate the src for the script tag
4
5
  * Refs: https://developers.google.com/recaptcha/docs/loading
@@ -9,4 +10,9 @@ export declare const getRecaptchaScriptSrc: ({ reCaptchaKey, language, useRecapt
9
10
  useRecaptchaNet?: boolean;
10
11
  useEnterprise?: boolean;
11
12
  }) => string;
12
- export declare const useIsomorphicLayoutEffect: typeof useLayoutEffect;
13
+ export declare const getGrecaptcha: (useEnterprise?: boolean) => IReCaptcha | null;
14
+ /**
15
+ * Hook to get the grecaptcha object
16
+ * @param useEnterprise - Use ReCaptcha Enterprise
17
+ */
18
+ export declare const useGrecaptcha: (useEnterprise?: boolean) => IReCaptcha | null;
package/lib/utils.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use client";
2
- import { useLayoutEffect, useEffect } from 'react';
2
+ import { useSyncExternalStore } from 'react';
3
3
 
4
+ const RECAPTCHA_LOADED_EVENT = "recaptcha_loaded";
4
5
  /**
5
6
  * Function to generate the src for the script tag
6
7
  * Refs: https://developers.google.com/recaptcha/docs/loading
@@ -15,7 +16,21 @@ const getRecaptchaScriptSrc = ({ reCaptchaKey, language, useRecaptchaNet = false
15
16
  src += `&hl=${language}`;
16
17
  return src;
17
18
  };
18
- // https://usehooks-ts.com/react-hook/use-isomorphic-layout-effect
19
- const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
19
+ const getGrecaptcha = (useEnterprise = false) => {
20
+ if (useEnterprise && window.grecaptcha?.enterprise)
21
+ return window.grecaptcha.enterprise;
22
+ return window.grecaptcha || null;
23
+ };
24
+ /**
25
+ * Hook to get the grecaptcha object
26
+ * @param useEnterprise - Use ReCaptcha Enterprise
27
+ */
28
+ const useGrecaptcha = (useEnterprise = false) => {
29
+ const grecaptcha = useSyncExternalStore((callback) => {
30
+ window.addEventListener(RECAPTCHA_LOADED_EVENT, callback);
31
+ return () => window.removeEventListener(RECAPTCHA_LOADED_EVENT, callback);
32
+ }, () => getGrecaptcha(useEnterprise), () => null);
33
+ return grecaptcha;
34
+ };
20
35
 
21
- export { getRecaptchaScriptSrc, useIsomorphicLayoutEffect };
36
+ export { RECAPTCHA_LOADED_EVENT, getGrecaptcha, getRecaptchaScriptSrc, useGrecaptcha };
@@ -1,14 +1,11 @@
1
1
  import React from "react";
2
2
  import type { useReCaptchaProps } from "./useReCaptcha.js";
3
- interface WithReCaptchaProps extends useReCaptchaProps {
4
- }
3
+ export type WithReCaptchaProps = useReCaptchaProps;
5
4
  /** React HOC to generate ReCaptcha token
6
5
  * @example
7
6
  * withReCaptcha(MyComponent)
8
7
  */
9
- declare function withReCaptcha<T extends WithReCaptchaProps = WithReCaptchaProps>(WrappedComponent: React.ComponentType<T>): {
8
+ export declare function withReCaptcha<T extends WithReCaptchaProps = WithReCaptchaProps>(WrappedComponent: React.ComponentType<T>): {
10
9
  (props: Omit<T, keyof WithReCaptchaProps>): React.JSX.Element;
11
10
  displayName: string;
12
11
  };
13
- export { withReCaptcha };
14
- export type { WithReCaptchaProps };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-recaptcha-v3",
3
- "version": "1.5.1-canary.0",
3
+ "version": "2.0.0-beta.1",
4
4
  "description": "🤖 Next.js hook to add Google ReCaptcha to your application",
5
5
  "license": "MIT",
6
6
  "author": "Roman Zhuravlov",
@@ -38,27 +38,31 @@
38
38
  "node": ">=18"
39
39
  },
40
40
  "devDependencies": {
41
- "@rollup/plugin-node-resolve": "^15.3.0",
42
- "@rollup/plugin-typescript": "^12.1.1",
43
- "@types/node": "^22.9.3",
44
- "@types/react": "^18.3.12",
45
- "@types/react-dom": "^18.3.1",
46
- "@typescript-eslint/eslint-plugin": "^7.18.0",
47
- "@typescript-eslint/parser": "^7.18.0",
48
- "eslint": "^8.57.1",
49
- "eslint-config-next": "^15.0.3",
50
- "eslint-config-prettier": "^9.1.0",
41
+ "@eslint/eslintrc": "^3.2.0",
42
+ "@eslint/js": "^9.20.0",
43
+ "@next/eslint-plugin-next": "^15.1.7",
44
+ "@rollup/plugin-node-resolve": "^16.0.0",
45
+ "@rollup/plugin-typescript": "^12.1.2",
46
+ "@types/node": "^22.13.4",
47
+ "@types/react": "^19.0.10",
48
+ "@types/react-dom": "^19.0.4",
49
+ "eslint": "^9.20.1",
50
+ "eslint-config-next": "^15.1.7",
51
+ "eslint-config-prettier": "^10.0.1",
52
+ "eslint-plugin-react": "^7.37.4",
53
+ "eslint-plugin-react-hooks": "^5.1.0",
51
54
  "husky": "^9.1.7",
52
- "lint-staged": "^15.2.10",
53
- "next": "^15.0.3",
54
- "prettier": "4.0.0-alpha.8",
55
+ "lint-staged": "^15.4.3",
56
+ "next": "^15.1.7",
57
+ "prettier": "3.5.1",
55
58
  "pretty-quick": "^4.0.0",
56
- "react": "^18.3.1",
57
- "react-dom": "^18.3.1",
58
- "rollup": "^4.27.4",
59
- "rollup-plugin-node-externals": "^7.1.3",
59
+ "react": "^19.0.0",
60
+ "react-dom": "^19.0.0",
61
+ "rollup": "^4.34.8",
62
+ "rollup-plugin-node-externals": "^8.0.0",
60
63
  "rollup-plugin-preserve-directives": "^0.4.0",
61
- "typescript": "^5.7.2"
64
+ "typescript": "^5.7.3",
65
+ "typescript-eslint": "^8.24.1"
62
66
  },
63
67
  "lint-staged": {
64
68
  "**/*.{js,jsx,ts,tsx}": [