Pesquisa de site

typescript - O elemento invisível do recaptcha do Firebase 9 está gerando o erro "o elemento cliente do reCAPTCHA foi removido"


Eu tenho uma aplicação de página única Vue 3 escrita em TypeScript, e usando Firebase versão 9 para Autenticação.

Usuários que estão conectados à sua conta podem verificar um número de telefone usando um código de verificação SMS único, e o número de telefone é então vinculado à sua conta de usuário.

Estas são minhas funções composáveis:

Cria um elemento reCaptcha obrigatório e o adiciona ao window:

const setInvisibleRecaptchaOnWindow = async (recaptchaId: string) => {
  window.recaptchaVerifier = new RecaptchaVerifier(
    recaptchaId,
    { size: 'invisible' },
    auth
  );
  window.recaptchaWidgetId = await window.recaptchaVerifier.render();
};

Redefine o reCaptcha na janela:

const resetReCaptchaOnWindow = () => {
  if (
    typeof grecaptcha !== 'undefined' &&
    typeof window.recaptchaWidgetId !== 'undefined'
  ) {
    grecaptcha.reset(window.recaptchaWidgetId);
  }
};

Envia o código de verificação por SMS:

const sendSMSOtp = async (phoneE164Standard: string, applicationVerifier: ApplicationVerifier) => {
    const provider = new PhoneAuthProvider(auth);
    const verificationId = await provider.verifyPhoneNumber(
      phoneE164Standard,
      applicationVerifier
    );
    return verificationId;
};

Verifica o código SMS e vincula o número de telefone à conta do usuário:

const verifySMSOtpAndLinkPhone = async (verificationId: string, verificationCode: string ) => {
    const phoneCredential = PhoneAuthProvider.credential(
      verificationId,
      verificationCode
    );
    if (user.value && user.value.phoneNumber) {
      // Remove existing number before adding a new one
      await unlink(user.value, 'phone');
      await linkWithCredential(user.value, phoneCredential);
    } else if (user.value) {
      // Add number new number if none previously existed
      await linkWithCredential(user.value, phoneCredential);
    }
};

Usando os composables dentro de um SFC do Vue:

Minha página possui vários campos para telefone e códigos de verificação, etc., além de uma div abaixo para armazenar o elemento reCaptcha. Aqui está uma versão simplificada:

<form @submit="handlePhoneSubmit">
  <input v-model="phone" type="text" placeholder="Phone Number">
  <input type="submit" value="Submit">
</form>
<form @submit="handleVerificationCodeSubmit">
  <input v-model="SMSOtp" type="text" placeholder="SMS Code">
  <input type="submit" value="Submit">
</form>
<div id="recaptcha-container"></div>

E estou usando isso para acionar as funções composables dentro da página Vue SFC.

let verificationId: string | undefined;

const handlePhoneSubmit = async () => {
  resetReCaptchaOnWindow();
  if (!window.recaptchaVerifier) {
      await setInvisibleRecaptchaOnWindow('recaptcha-container');
  }
  if (window.recaptchaVerifier) {
    verificationId = await sendSMSOtp(
      phoneE164Standard.value,
      window.recaptchaVerifier
    );
  }
};

const handleVerificationCodeSubmit = async () => {
  await verifySMSOtpAndLinkPhone(verificationId, SMSOtp.value);
};

Tudo funciona muito bem se o usuário permanecer na mesma página. Eles podem alterar e verificar seu número de telefone quantas vezes precisarem.

Mas se eles navegarem para uma "página" diferente do SPA e voltarem para tentar mudar o número novamente, eu recebo este erro:

reCAPTCHA client element has been removed: 0

Este é um SPA, então navegar para outra página não atualiza a página. Isso apenas altera os elementos DOM.

E eu presumo que o erro seja porque o reCaptcha foi criado na div recaptcha-container, e ao navegar para fora essa div é removida. Então, ao retornar, não há mais um elemento reCaptcha.

Tentei remediar isso usando o resetReCaptchaOnWindow() antes de chamar as funções de verificação a cada vez. O código foi retirado e ajustado a partir deste exemplo de código de inicialização rápida do Firebase. Mas não faz nada. O erro ainda é lançado.

Como posso resolver este problema?

Resposta #1

Resolvi isso movendo <div id="recaptcha-container"></div> para App.vue para que nunca fosse deletado ao trocar de página:

<template>
  <router-view />
  <div id="recaptcha-container"></div>
</template>

E aqui estão os composables que acabei usando. Não é necessário resetReCaptchaOnWindow.

Observe que os composables abaixo estão envoltos com um gerador Vue Concurrency useTask. Você não precisa fazer isso se não quiser, mas o Vue Concurrency é uma biblioteca realmente legal que ajuda a reduzir muito código repetitivo em muitas situações (não apenas nesta).

export const useSendSMSOtpTask = () => {
  return useTask(function* (
    signal,
    phoneE164Standard: string,
    recaptchaConatinerId: string
  ) {
    if (!window.recaptchaVerifier) {
      window.recaptchaVerifier = new RecaptchaVerifier(
        recaptchaConatinerId,
        { size: 'invisible' },
        auth
      );
    }
    const provider = new PhoneAuthProvider(auth);
    const verificationId: string = yield provider.verifyPhoneNumber(
      phoneE164Standard,
      window.recaptchaVerifier
    );
    return verificationId;
  });
};

export const useVerifySMSOtpAndLinkPhoneTask = () => {
  return useTask(function* (
    signal,
    verificationId: string,
    verificationCode: string
  ) {
    const phoneCredential = PhoneAuthProvider.credential(
      verificationId,
      verificationCode
    );
    if (auth.currentUser && auth.currentUser.phoneNumber) {
      // Remove existing phone then add new phone
      yield unlink(auth.currentUser, 'phone');
      yield linkWithCredential(auth.currentUser, phoneCredential);
    } else if (auth.currentUser) {
      // Add new phone
      yield linkWithCredential(auth.currentUser, phoneCredential);
    }
    return auth.currentUser?.phoneNumber;
  });
};