网站搜索

typescript - Firebase 9 隐形 reCAPTCHA 元素抛出“reCAPTCHA 客户端元素已被移除”错误


我有一个用 TypeScript 编写的 Vue 3 单页面应用,并使用 Firebase 9 版本进行身份验证。

已登录账户的用户可以使用一次性短信验证码验证电话号码,然后该电话号码将与其用户账户关联。

这些是我的可组合函数:

创建一个必需的 reCaptcha 元素并将其添加到 window:

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

重置 window 上的 reCaptcha:

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

发送短信验证码:

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

验证短信代码并将电话号码绑定到用户账户:

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

在 Vue 单文件组件中使用组合函数:

我的页面有各种用于手机和验证码等的输入框,以及一个下面的 div 用来存放 reCaptcha 元素。这里是一个简化版本:

<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>

我正在使用这个来触发 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);
};

如果用户停留在同一页面,一切运行良好。他们可以根据需要多次更改和验证自己的电话号码。

但是如果他们导航到 SPA 的不同“页面”然后再回来尝试再次更改他们的号码,我就会收到这个错误:

reCAPTCHA client element has been removed: 0

这是一个单页应用,所以导航到其他页面不会刷新页面。它只会改变 DOM 元素。

我假设这个错误是因为 reCaptcha 是在 recaptcha-container div 上创建的,而当导航离开时,该 div 被移除了。因此返回时已经没有 reCaptcha 元素了。

我试图通过在每次调用验证函数之前使用 resetReCaptchaOnWindow() 来解决这个问题。这段代码是从这个 Firebase 快速入门示例中获取并调整的。但它没有任何作用。错误仍然会出现。

我该如何解决这个问题?

答案#1

我通过将 <div id="recaptcha-container"></div> 移动到 App.vue 来解决这个问题,这样在切换页面时它就不会被删除:

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

这里是我最终使用的可组合函数。不需要 resetReCaptchaOnWindow。

请注意,以下可组合函数都被包装在 Vue Concurrency 的 useTask 生成器中。如果你不想,也不需要这样做,但 Vue Concurrency 是一个非常酷的库,它可以在许多情况下(不仅仅是这一种)大大减少大量样板代码。

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