firebase-js-sdk icon indicating copy to clipboard operation
firebase-js-sdk copied to clipboard

grecaptcha keeps a reference to my div in window.___grecaptcha_cfg.clients even after RecaptchaVerifier.clear()

Open andresemartinez opened this issue 1 year ago • 0 comments

Operating System

macOS 14.2 (23C64)

Browser Version

Chrome 121.0.6167.160 (Official Build) (arm64)

Firebase SDK Version

10.8.0

Firebase SDK Product:

Auth

Describe your project's tooling

Next.js 14

Describe the problem

When reloading my components (because some state changed or a code change triggered hot reload) I clear the RedirectVerifier and the innerHtml of the container div. Then when I create a new RecaptchaVerifier, pass the same div as container and try to render it I get an Error: reCAPTCHA has already been rendered in this element.

Using the chrome devtools I found out that grecaptcha has a reference to my div in window.___grecaptcha_cfg.clients even after I use RecaptchaVerifier.clear(). So when I try to render the new RecaptchaVerifier using the same div it breaks even if it is empty and the previos ReaptchaVerifier no longer exists.

I can work around this by manually clearing that reference between rerenders or by using a new div so it's not the same reference but I think RecaptchaVerifier.clear() should take care of this.

Steps and code to reproduce issue

Steps

  1. Have an empty div
  2. Create a RecaptchaVerifier using the div created in step 1 as container
  3. Render the RecaptchaVerifier (RecaptchaVerifier.render())
  4. Clear the RecaptchaVerifier (RecaptchaVerifier.render())
  5. Clear the container div (containerRef.innerHtml = '')
  6. Repeat steps 2 to 4

My Code

Recaptcha container

export type RecaptchaProps = {
  containerRef: React.Ref<HTMLDivElement>;
};

export const Recaptcha = ({ containerRef }: RecaptchaProps) => {
  return <div ref={containerRef} />;
};

RecaptchaVerifier

import { getAuth, RecaptchaVerifier } from 'firebase/auth';
import { useCallback, useEffect, useState } from 'react';

export const useRecaptcha = () => {
  const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null);
  const [verifier, setVerifier] = useState<RecaptchaVerifier>();

  const createVerifier = useCallback((containerRef: HTMLDivElement | null) => {
    let verifier: RecaptchaVerifier | undefined;

    if (containerRef !== null) {
      console.log(containerRef);
      verifier = new RecaptchaVerifier(getAuth(), containerRef, {
        size: 'invisible',
      });
    }

    return verifier;
  }, []);

  const clear = useCallback(
    (
      verifier: RecaptchaVerifier | undefined,
      containerRef: HTMLDivElement | null,
    ) => {
      verifier?.clear();
      if (containerRef !== null) {
        containerRef.innerHTML = '';
      }
    },
    [],
  );

  useEffect(() => {
    const newVerifier = createVerifier(containerRef);
    newVerifier?.render().then(
      () => {
        setVerifier(newVerifier);
      },
      (error) => console.log(error),
    );

    return () => clear(newVerifier, containerRef);
  }, [createVerifier, containerRef, setVerifier, clear]);

  return { verifier, containerRef: setContainerRef };
};

Login Page

const LoginPage = () => {
    const { verifier, containerRef } = useRecaptcha()
    [...]
    return (
        <div>
            [...]
            <Recaptcha containerRef={containerRef}/>
        </div>
    );
}

andresemartinez avatar Feb 14 '24 16:02 andresemartinez