Reputation: 2103
I have a Vue 3 single page application written in TypeScript, and using Firebase version 9 for Authentication.
Users who are signed into their account can verify a phone number using a one time SMS verification code, and the phone number is then linked to their user account.
These are my composable functions:
Creates a required reCaptcha element and adds it to the window
:
const setInvisibleRecaptchaOnWindow = async (recaptchaId: string) => {
window.recaptchaVerifier = new RecaptchaVerifier(
recaptchaId,
{ size: 'invisible' },
auth
);
window.recaptchaWidgetId = await window.recaptchaVerifier.render();
};
Resets the reCaptcha on the window
:
const resetReCaptchaOnWindow = () => {
if (
typeof grecaptcha !== 'undefined' &&
typeof window.recaptchaWidgetId !== 'undefined'
) {
grecaptcha.reset(window.recaptchaWidgetId);
}
};
Sends the SMS verification code:
const sendSMSOtp = async (phoneE164Standard: string, applicationVerifier: ApplicationVerifier) => {
const provider = new PhoneAuthProvider(auth);
const verificationId = await provider.verifyPhoneNumber(
phoneE164Standard,
applicationVerifier
);
return verificationId;
};
Verifies the SMS code and links the phone number to the user account:
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);
}
};
Using the composables inside a Vue SFC:
My page has various inputs for phone and verification codes etc plus a div underneath to store the reCaptacha element. Here is a simplified version:
<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>
And I am using this to trigger the composable functions inside the Vue SFC page.
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);
};
Everything works great if the user stays on the same page. They can change and verify their phone number as many times as they need.
But if they navigate away to a different "page" of the SPA and come back to try and change their number again, I get this error:
reCAPTCHA client element has been removed: 0
This is a SPA, so navigating away doesn't refresh the page. It just changes the DOM elements.
And I assume the error is because the reCaptcha was created on the recaptcha-container
div, and when navigating away that div gets removed. So upon return there is no longer a reCaptcha element.
I tried to remedy this by using the resetReCaptchaOnWindow()
before calling the verification functions each time. The code was taken and adjusted from this Firebase quick start code example. But it doesn't do anything. The error is still thrown.
How can I fix this problem?
Upvotes: 2
Views: 3629
Reputation: 2103
I solved this by moving <div id="recaptcha-container"></div>
into App.vue
so it would never be deleted when changing pages:
<template>
<router-view />
<div id="recaptcha-container"></div>
</template>
And here are the composables I ended up using. No need for resetReCaptchaOnWindow
.
Note the below composables are wrapped with a Vue Concurrnecy useTask
generator. You don't need to do that if you don't want, but Vue Concurrnecy is a seriously cool library that helps cut down on a ton of boiler plate code in a lot of situations (not just this one).
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;
});
};
Upvotes: 2