snelsi / next-recaptcha-v3

โญ Straightforward solution for using ReCaptcha in your Next.js application
MIT License
108 stars 8 forks source link

Next.js 13.2: Error on `executeRecaptcha` #135

Closed Antonio112009 closed 1 year ago

Antonio112009 commented 1 year ago

Describe the bug In the Nextjs 13.2, the token is "not loaded" when I request that the reCaptcha server generate a token. The server responds with the following message:

Uncaught (in promise) Error: Recaptcha has not been loaded
    at eval (webpack-internal:///(:3000/app-client)/./node_modules/next-recaptcha-v3/lib/index.esm.js:140:19)
    at Generator.next (<anonymous>)
    at eval (webpack-internal:///(:3000/app-client)/./node_modules/next-recaptcha-v3/lib/index.esm.js:52:71)
    at new Promise (<anonymous>)
    at __awaiter (webpack-internal:///(:3000/app-client)/./node_modules/next-recaptcha-v3/lib/index.esm.js:48:12)
    at eval (webpack-internal:///(:3000/app-client)/./node_modules/next-recaptcha-v3/lib/index.esm.js:138:93)
    at onSubmit (webpack-internal:///(app-client)/./src/app/(components)/(:3000/forms)/SubscribeFormComponent.js:37:29)
    at eval (webpack-internal:///(:3000/app-client)/./node_modules/react-hook-form/dist/index.esm.mjs:2021:19)

I am using v1.14 of the next-recaptcha-v3

To Reproduce Steps to reproduce the behavior:

app/layout.js

import '../_css/global.css';
import Header from "../components/header/Header";
import Footer from "../components/footer/Footer";
import qs from "qs";
import axios from "axios";
import {ReCaptchaProvider} from "next-recaptcha-v3";

const querySearch = qs.stringify({populate: ['navigation', 'social_media']}, {encodeValuesOnly: true});

async function getPageData() {
    const response = await axios.get(`...?${querySearch}`);
    return response;
}

export default async function RootLayout({children}) {

    const globalCong = (await getPageData()).data.data;

    return (
        <html lang="en">
        <head/>
        <body>
        <ReCaptchaProvider reCaptchaKey={"......"}>
            <Header globalData={globalCong}/>
            <div className={"page"}>
                {children}
            </div>
            <Footer globalData={globalCong}/>
        </ReCaptchaProvider>
        </body>
        </html>
    )
}

app/contact-us/page.js

"use client"

import React from 'react';
import {useForm} from "react-hook-form";
import axios from "axios";
import {useReCaptcha} from "next-recaptcha-v3";

const SubscribeFormComponent = () => {
    const {executeRecaptcha} = useReCaptcha();

    const { register, handleSubmit, formState: { errors }, reset } = useForm({
        defaultValues: {
            firstname: "",
            lastname: "",
            email: "",
            company: "",
            telephone: "",
            message: ""
        }
    });

    const [isSuccessfullySubmitted, setIsSuccessfullySubmitted] = React.useState(null);

    const onSubmit = async (data) => {
                console.log("data", data);
                const token = await executeRecaptcha('submitForm');
                console.log(token)
                // await axios.post('....', {
                //     token: token,
                //     formData: data})
                //     .then((res) => {
                //         setIsSuccessfullySubmitted(true);
                //     })
                //     .catch((error) => {
                //         setIsSuccessfullySubmitted(false);
                //     }).finally(() => {
                // });
                reset()
    }

Expected behavior A clear and concise description of what you expected to happen. The expected behaviour is that on submit, a token should be returned.

Screenshots If applicable, add screenshots to help explain your problem.

Desktop (please complete the following information):

Smartphone (please complete the following information):

Additional context Add any other context about the problem here.

snelsi commented 1 year ago

Yeah, I see a very weird behavior in the CodeSandbox:

https://codesandbox.io/p/sandbox/next-js-13-app-broken-b3qdyi

On the initial render context from the ReCaptchaProvider loads fine, but after hydration it fallbacks to default values (nulls):

image

It's relatively easy to workaround on your side, by wrapping ReCaptchaProvider into a separate client component:

https://codesandbox.io/p/sandbox/next-js-13-app-fixed-ujphw7

"use client";

import React from "react";
import { ReCaptchaProvider } from "next-recaptcha-v3";

const Providers = ({ children }) => (
  <ReCaptchaProvider reCaptchaKey="6LfD67gkAAAAAP38z-nzxQ9YjhJsZAiIO0dpTVNi">
    {children}
  </ReCaptchaProvider>
);

export default Providers;

image

Looks like the client loses the context when doing hydration. Not sure why it happens, though. Our build file is marked as "use client", so it should work fine both on the server and client.

Will probably need some help with this ๐Ÿคจ

snelsi commented 1 year ago

Related: https://beta.nextjs.org/docs/rendering/server-and-client-components#rendering-third-party-context-providers-in-server-components

snelsi commented 1 year ago

Will probably need to wait for this thing to resolve first: https://github.com/rollup/rollup/issues/4699

Antonio112009 commented 1 year ago

@snelsi Thank you so much for a quick fix! Really appreciate your work.

prokopsimek commented 1 year ago

@snelsi It seems that the rollup issue has already been resolved.

https://github.com/rollup/rollup/issues/4699 leads to merged PR: https://github.com/inokawa/virtua/pull/103

Can you try to update this lib?

snelsi commented 1 year ago

@prokopsimek I've changed the rollup config to preserveModules: true + added preserveDirectives plugin. You can test this in 1.2.0-canary.0 version by running npm i next-recaptcha-v3@1.2.0-canary.0

prokopsimek commented 1 year ago

@snelsi It didn't help. :( image

The ReCaptcha works when I wrap the in my layout.tsx with component, but it's visible on all pages then. This "Recaptcha has not been loaded" error is happening in my Form component.

snelsi commented 1 year ago

@prokopsimek Can confirm that both cjs and esm output files include 'use client'; at the top ๐Ÿค” Maybe, the Recaptcha has not been loaded error not necessarily relates to RSC

snelsi commented 1 year ago

I have no idea why this is happening, but again:

This works: โœ…

providers.tsx

"use client";

import { ReCaptchaProvider } from "next-recaptcha-v3";

const Providers = ({ children }) => <ReCaptchaProvider>{children}</ReCaptchaProvider>

export default Providers;

layout.tsx

import Providers from "./providers";

const Layout = ({ children }) => <Providers>{children}</Providers>

export default Layout;

This doesn't work: โŒ

layout.tsx

import { ReCaptchaProvider } from "next-recaptcha-v3";

const Layout = ({ children }) => <ReCaptchaProvider>{children}</ReCaptchaProvider>

export default Layout;

useReCaptcha hook fallbacks to default placeholder value after hydration. It's like it doesn't see a context provider above it at all.

Could it be caused by server using cjs and client using esm...? ๐Ÿคจ

prokopsimek commented 1 year ago

You're right.

This doesn't work either: โŒ

components/Form.tsx

import { ReCaptchaProvider } from "next-recaptcha-v3";

const Form = ({ children }) => <ReCaptchaProvider>{children}</ReCaptchaProvider>

export default Form;

I'm not sure what's causing it. Any idea, @jdckl?

snelsi commented 1 year ago

1.2.0-canary.3 version is now ESM ONLY

But it finally works in my localhost ๐ŸŽ‰ image

prokopsimek commented 1 year ago

Still not for me. package.json "next-recaptcha-v3": "1.2.0-canary.3",

src/app/components/Providers.tsx

'use client';

import React from 'react';
import { ReCaptchaProvider } from 'next-recaptcha-v3';

const Providers = ({ children }: { children: React.ReactNode }) => (
  <ReCaptchaProvider reCaptchaKey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY}>{children}</ReCaptchaProvider>
);

export default Providers;

src/app/components/ResourceForm.tsx

'use client';

import Link from 'next/link';
import Button from './Button';
import React, { useCallback, useState } from 'react';
import handleFormChange from './base/handleFormChange';
import { toast } from 'react-hot-toast';
import toastTemplate from './base/toastTemplate';
import axios from 'axios';
import { useReCaptcha } from 'next-recaptcha-v3';
import Providers from './Providers';

export default function ResourceForm(props: { mailchimpListId: string; tags: string }) {
  const { mailchimpListId, tags } = props;
  const [privacyAgreed, setPrivacyAgreed] = useState(false);
  const [formData, setFormData] = useState({
    email: '',
  });

  const { executeRecaptcha, loaded, error } = useReCaptcha();

  const handleSubmit = useCallback(
    async (e: React.FormEvent<HTMLFormElement>) => {
      e.preventDefault();

      if (!privacyAgreed) {
        toast.custom((t) => toastTemplate(t, 'error', 'Uh oh!', 'You need to read and agree with the privacy policy!'), { id: 'privacy' });
        return;
      }

      try {
        //
      } catch (e) {
        //
      }
    },
    [executeRecaptcha, formData, mailchimpListId, tags, privacyAgreed]
  );

  return (
    <Providers>
      <form className="mt-12" onSubmit={handleSubmit}>
        <label htmlFor="email" className="sr-only mb-2 text-sm font-medium text-gray-900 dark:text-white">
          Download
        </label>
        <div className="flex flex-col items-stretch md:flex-row">
          <input
            defaultValue={formData.email}
            name="email"
            onChange={(e) => handleFormChange(e, setFormData, formData)}
            type="email"
            id="email"
            className="bg-white px-4 py-5 text-gray-900 md:grow"
            placeholder="Your work email"
            required
          />
          <Button
            className="mt-4 flex w-full items-center justify-center md:mt-0 md:w-auto md:justify-start"
            label={'Download' + loaded + error}
            formSubmit
            style="primary"
          />
        </div>
        <div className="mt-6 flex flex-row gap-2">
          <div className="flex h-6 items-center">
            <input
              id="remember"
              type="checkbox"
              value=""
              onClick={() => setPrivacyAgreed(!privacyAgreed)}
              defaultChecked={privacyAgreed}
              className="h-6 w-6 border border-gray-300 bg-white accent-dxteal ring-dxteal checked:bg-dxteal hover:checked:bg-dxteal focus:outline-none focus:ring-dxteal focus:checked:bg-dxteal active:bg-dxteal"
            />
          </div>
          <label htmlFor="remember" className="ml-2 cursor-pointer select-none font-medium text-white">
            I agree with the{' '}
            <Link href="/privacy-policy.pdf" className="underline transition-all duration-300 ease-in-out hover:text-dxteal">
              privacy policy
            </Link>
            .
          </label>
        </div>
      </form>
    </Providers>
  );
}
Error: Recaptcha has not been loaded
    at eval (webpack-internal:///(app-client)/./node_modules/next-recaptcha-v3/lib/useReCaptcha.js:27:19)
    at eval (webpack-internal:///(app-client)/./src/app/components/ResourceForm.tsx:46:33)
    at HTMLUnknownElement.callCallback (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:19449:14)
    at Object.invokeGuardedCallbackImpl (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:19498:16)
    at invokeGuardedCallback (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:19573:29)
    at invokeGuardedCallbackAndCatchFirstError (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:19587:25)
    at executeDispatch (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:30622:3)
    at processDispatchQueueItemsInOrder (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:30654:7)
    at processDispatchQueue (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:30667:5)
    at dispatchEventsForPlugins (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:30678:3)
    at eval (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:30868:12)
    at batchedUpdates$1 (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:23765:12)
    at batchedUpdates (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:27584:12)
    at dispatchEventForPluginEventSystem (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:30867:3)
    at dispatchEvent (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:28640:5)
    at dispatchDiscreteEvent (webpack-internal:///(app-client)/./node_modules/next/dist/compiled/react-dom/cjs/react-dom.development.js:28611:5)
snelsi commented 1 year ago

@prokopsimek

1) You can safely remove this code: reCaptchaKey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY}> Provider will link it automatically.

2) You're trying to use useReCaptcha outside of the ReCaptchaProvider.

Context exists only for children inside of the provider. And in your case useReCaptcha hook is located outside of it.

This template won't work:

const Component = () => {
  const {} = useContext()

  return (
    <Context.Provider>
      {children}
    </Context.Provider>
   )
}

Move your Provider up, like this:

<Context.Provider>
  <ResourceForm />
</Context.Provider>
prokopsimek commented 1 year ago

@snelsi I have moved it and the same error.

components/pages/ResourcesSection.tsx

'use client';

import Section from '../Section';
import Container from '../Container';
import { CMSClient } from '@/client/cmsClient';
import { LayoutResourcesComponent } from '@/client/sdk';
import Button from '../Button';
import { FiDownload } from 'react-icons/fi';
import Image from 'next/image';
import { BaseAnim } from '../base/baseAnim';
import Providers from '../Providers';

export default async function ResourcesSection(props: LayoutResourcesComponent) {
  const resources = (await CMSClient.get.resources())?.data;

  return (
    <Providers>
      <Section className="bg-light-700 py-16">
        <Container>
          <div className="grid grid-cols-1 gap-8 px-8 md:grid-cols-2 md:px-0">
            {resources?.map((resource, i) => {
              if (resource.attributes) {
                const { title, slug, description, image } = resource.attributes;

                return (
                  <BaseAnim key={i} className="flex flex-col items-center justify-center gap-10 border-2 border-white bg-white md:flex-row">
                    <div className="relative h-full overflow-hidden md:basis-1/3">
                      {image.data?.attributes?.url && (
                        <Image
                          className="min-h-full object-none"
                          alt={image.data?.attributes?.alternativeText || ''}
                          src={image.data?.attributes?.url}
                          width={image.data.attributes.width}
                          height={image.data.attributes.height}
                        />
                      )}
                    </div>
                    <div className="flex flex-col gap-6 p-8 md:basis-2/3">
                      <h3>{title}</h3>
                      <p>{description}</p>
                      <Button icon={FiDownload} className="mt-6" href={`/resources/${slug}`} label="Download for free" style="secondary" />
                    </div>
                  </BaseAnim>
                );
              }
            })}
          </div>
        </Container>
      </Section>
    </Providers>
  );
}

components/ResourceForm.tsx

'use client';

import Link from 'next/link';
import Button from './Button';
import React, { useCallback, useState } from 'react';
import handleFormChange from './base/handleFormChange';
import { toast } from 'react-hot-toast';
import toastTemplate from './base/toastTemplate';
import axios from 'axios';
import { useReCaptcha } from 'next-recaptcha-v3';

export default function ResourceForm(props: { mailchimpListId: string; tags: string }) {
  const { mailchimpListId, tags } = props;
  const [privacyAgreed, setPrivacyAgreed] = useState(false);
  const [formData, setFormData] = useState({
    email: '',
  });

  const { executeRecaptcha, loaded, error } = useReCaptcha();

  const handleSubmit = useCallback(
    async (e: React.FormEvent<HTMLFormElement>) => {
      e.preventDefault();

      if (!privacyAgreed) {
        toast.custom((t) => toastTemplate(t, 'error', 'Uh oh!', 'You need to read and agree with the privacy policy!'), { id: 'privacy' });
        return;
      }

      try {
        // Generate ReCaptcha token
        const token = await executeRecaptcha('form_submit');

        await axios.post('/api/form', { data: formData, listId: mailchimpListId, tags, token });
        toast.custom((t) => toastTemplate(t, 'success', 'Good job!', 'You will receive the resource to your email momentarily!'));
      } catch (e) {
        console.error(e);
        toast.custom((t) => toastTemplate(t, 'error', 'Something went bad!', 'Let us know, this should not be happening!'), {
          id: 'error',
        });
      }
    },
    [executeRecaptcha, formData, mailchimpListId, tags, privacyAgreed]
  );

  return (
    <form className="mt-12" onSubmit={handleSubmit}>
      <label htmlFor="email" className="sr-only mb-2 text-sm font-medium text-gray-900 dark:text-white">
        Download
      </label>
      <div className="flex flex-col items-stretch md:flex-row">
        <input
          defaultValue={formData.email}
          name="email"
          onChange={(e) => handleFormChange(e, setFormData, formData)}
          type="email"
          id="email"
          className="bg-white px-4 py-5 text-gray-900 md:grow"
          placeholder="Your work email"
          required
        />
        <Button
          className="mt-4 flex w-full items-center justify-center md:mt-0 md:w-auto md:justify-start"
          label={'Download' + loaded + error}
          formSubmit
          style="primary"
        />
      </div>
      <div className="mt-6 flex flex-row gap-2">
        <div className="flex h-6 items-center">
          <input
            id="remember"
            type="checkbox"
            value=""
            onClick={() => setPrivacyAgreed(!privacyAgreed)}
            defaultChecked={privacyAgreed}
            className="h-6 w-6 border border-gray-300 bg-white accent-dxteal ring-dxteal checked:bg-dxteal hover:checked:bg-dxteal focus:outline-none focus:ring-dxteal focus:checked:bg-dxteal active:bg-dxteal"
          />
        </div>
        <label htmlFor="remember" className="ml-2 cursor-pointer select-none font-medium text-white">
          I agree with the{' '}
          <Link href="/privacy-policy.pdf" className="underline transition-all duration-300 ease-in-out hover:text-dxteal">
            privacy policy
          </Link>
          .
        </label>
      </div>
    </form>
  );
}

components/Providers.tsx

'use client';

import React from 'react';
import { ReCaptchaProvider } from 'next-recaptcha-v3';

const Providers = ({ children }: { children: React.ReactNode }) => <ReCaptchaProvider>{children}</ReCaptchaProvider>;

export default Providers;

Could it be because I am using it in a component and not in a page? ๐Ÿคท

image As you can see, there is false/false in the button. It's loaded/error.

And the same error in console. :/

snelsi commented 1 year ago

@prokopsimek I don't see ResourceForm component inside of your ResourcesSection. Where do you render it?

prokopsimek commented 1 year ago

@snelsi I am an idiot! ๐Ÿ˜‚ It had to be inserted in resource/page.tsx instead of my ResourceSection.tsx.

It works. Thanks! ๐Ÿฅ๐ŸŽ‰

snelsi commented 1 year ago

This bug should hopefully be fixed in the 1.2.0 release ๐Ÿคž

GoudekettingRM commented 1 year ago

Hi @snelsi,

Still seeing this error.

Versions: next@13.4.10 next-recaptcha-v3@1.2.0 react-hook-form@7.44.3

Using a custom component for RHF to load the grecaptcha_response to send to my api as follows:

import { useReCaptcha } from 'next-recaptcha-v3';
import { useCallback, useEffect, useState } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { type TInputProps } from './formInputProps';

const CaptchaInput = ({
  control,
  cta,
}: Pick<TInputProps, 'control'> & { cta: string }): JSX.Element => {
  const [token, setToken] = useState('');
  const { setValue } = useFormContext();
  const { executeRecaptcha } = useReCaptcha();

  const handleToken: (token: string) => void = useCallback(
    (token: string): void => {
      setToken(token);
      setValue('grecaptcha_response', token);

      setTimeout(() => {
        const loadToken = async (): Promise<void> => {
          const newToken = await executeRecaptcha(cta);

          handleToken(newToken);
        };
        loadToken();
      }, 120000);
    },
    [executeRecaptcha, setValue, cta],
  );

  useEffect(() => {
    const loadToken = async (): Promise<void> => {
      const newToken = await executeRecaptcha(cta);

      handleToken(newToken);
    };
    loadToken();
  }, [cta, executeRecaptcha, handleToken]);

  return (
    <Controller
      name='grecaptcha_response'
      control={control}
      render={({ field: { onChange } }) => (
        <input type='hidden' value={token} name='grecaptcha_response' onChange={onChange} />
      )}
    />
  );
};

export default CaptchaInput;

Have the ReCaptchaProvider in my _app.tsx.

<ReCaptchaProvider reCaptchaKey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY ?? ''}>
  <Component {...pageProps} />
</ReCaptchaProvider>

(And yes, the process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY has a value.)

The specific error comes from the executeRecaptcha in the useEffect. Screenshot 2023-07-25 at 12 12 35

Any ideas?

snelsi commented 1 year ago

@GoudekettingRM I feel like your code is a bit overenginired.

  1. You can safely remove reCaptchaKey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY ?? ''}. ReCaptchaProvider fallbacks to using process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY by default.

  2. Try checking if loaded param is true before calling executeRecaptcha. ReCaptcha script takes some time to load, so you usually can't immediately generate a token on a page load.

I would suggest to update your code to look something like this:

import { useReCaptcha } from 'next-recaptcha-v3';
import { useEffect} from 'react';
import { useFormContext } from 'react-hook-form';

const CaptchaInput = ({
  name = "grecaptcha_response",
  cta = "form_submit",
}) => {
  const form = useFormContext();
  const { loaded, executeRecaptcha } = useReCaptcha();

  useEffect(() => {
    const loadToken = async () => {
      const token = await executeRecaptcha(cta);
      form.setValue(name, token);
    };
    if (loaded) loadToken();
  }, [cta, loaded, executeRecaptcha]);

  return <input {...form.register(name)} type='hidden' />;
};

export default CaptchaInput;
GoudekettingRM commented 1 year ago

@snelsi Haha, I just wanted to reply that I read through the docs again and indeed am using the loaded state from the hook now. That works like a charm. Thanks a lot!

snelsi commented 1 year ago

@GoudekettingRM Glad you managed to make it work ๐ŸŽ‰

If you want, you can check out the code snippet I've posted above to simplify the logic inside of your component a bit.

If you have any additional questions, feel free to open a separate issue

GoudekettingRM commented 1 year ago

@snelsi Yeah, I made it a bit simpler based of your snippet. Only thing we do seem to need is the refresh after 2 minutes, since google invalidates the tokens after that time. Have it like this now:

import { useReCaptcha } from 'next-recaptcha-v3';
import { useEffect } from 'react';
import { useFormContext } from 'react-hook-form';

const CaptchaInput = ({ cta }: { cta: string }): JSX.Element => {
  const { setValue, register } = useFormContext();
  const { executeRecaptcha, loaded } = useReCaptcha();

  useEffect(() => {
    let time: NodeJS.Timeout | null = null;

    const loadToken = async (): Promise<void> => {
      const token = await executeRecaptcha(cta);
      setValue('grecaptcha_response', token);

      time = setInterval(() => {
        if (time) clearInterval(time);

        loadToken();
      }, 120000);
    };

    if (loaded) loadToken();

    return () => {
      if (time) clearInterval(time);
    };
  }, [cta, loaded, executeRecaptcha, setValue]);

  return <input type='hidden' {...register('grecaptcha_response')} />;
};

export default CaptchaInput;
itsaalexk commented 2 months ago

Hello! Im getting the same error using Nextjs with recaptcha v3 version. I still dont know what im doing wrong. I use the hook inside the provider. I can see the captcha being loaded on the website but it seems that the script is not loading since im getting the same error. I use the 1.4 version of next-recaptcha-v3.

Here is my code:

`export default function useCaptcha() { const { executeRecaptcha, loaded } = useReCaptcha()

const handleValidateCaptcha = useCallback(async () => {
    try {
        const captchaToken = await executeRecaptcha('click')
        const response = await fetch(
            'http://localhost:4321/api/recaptcha',
            {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ token: captchaToken }),
            },
        )

        return response
    } catch (error) {
        console.error('Captcha validation error:', error)
    }
}, [executeRecaptcha, loaded])

const Providers = ({ children }: CaptchaProperties) => {
    return (
        <ReCaptchaProvider reCaptchaKey="">
            {children}
        </ReCaptchaProvider>
    )
}

return {
    Providers,
    handleValidateCaptcha,
}

} `

Im using the environment variable, im just ommiting it for safety

I created a custom hook that returns the provider and a function that handles all the logic. Then I use a form inside the children on the provider.

All of the components are use client.