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;
});
};
