Fix email verif not refreshing component

This commit is contained in:
MartinBraquet
2026-02-13 21:03:49 +01:00
parent 7928e58d3b
commit e8cfc77902
7 changed files with 81 additions and 24 deletions

View File

@@ -1,19 +1,34 @@
import {User} from 'firebase/auth'
import {Button} from 'web/components/buttons/button'
import {sendVerificationEmail} from 'web/lib/firebase/email-verification'
import {useT} from 'web/lib/locale'
import {Col} from "web/components/layout/col"
import toast from "react-hot-toast";
import {useFirebaseUser} from "web/hooks/use-firebase-user";
export function EmailVerificationButton(props: {
user: User
}) {
const {user} = props
export function EmailVerificationButton() {
const user = useFirebaseUser()
const t = useT()
const isEmailVerified = user.emailVerified
const isEmailVerified = user?.emailVerified
async function reload() {
if (!user) return false;
// Refresh user record from Firebase
await user.reload();
if (user.emailVerified) {
// IMPORTANT: force a new ID token with updated claims
await user.getIdToken(true)
console.log("User email verified")
return true
} else {
toast.error(t('', "Email still not verified..."))
}
}
return (
<Col>
<Col className={'gap-2 mx-4'}>
<Button
color={'gray-outline'}
onClick={() => sendVerificationEmail(user, t)}
@@ -24,6 +39,15 @@ export function EmailVerificationButton(props: {
? t('settings.email.verified', 'Email Verified ✔️')
: t('settings.email.send_verification', 'Send verification email')}
</Button>
<div className={'custom-link'}>
<button
type="button"
onClick={reload}
className={'w-fit mx-2'}
>
{t('settings.email.just_verified', 'I verified my email')}
</button>
</div>
</Col>
)
}

View File

@@ -3,21 +3,19 @@ import clsx from 'clsx'
import {EmailVerificationButton} from "web/components/email-verification-button";
interface EmailVerificationPromptProps {
firebaseUser: any
t: any
className?: string
}
export function EmailVerificationPrompt(
{
firebaseUser,
t,
className
}: EmailVerificationPromptProps) {
return (
<Col className={clsx('gap-4 max-w-xl', className)}>
<h3>{t('messaging.email_verification_required', "You must verify your email to message people.")}</h3>
<EmailVerificationButton user={firebaseUser}/>
<EmailVerificationButton/>
</Col>
)
}

View File

@@ -15,10 +15,11 @@ import {MAX_COMMENT_LENGTH} from 'common/comment'
import {CommentInputTextArea} from 'web/components/comments/comment-input'
import {Title} from 'web/components/widgets/title'
import {Row} from 'web/components/layout/row'
import {auth, firebaseLogin} from 'web/lib/firebase/users'
import {firebaseLogin} from 'web/lib/firebase/users'
import {useT} from 'web/lib/locale'
import {Tooltip} from "web/components/widgets/tooltip";
import {EmailVerificationPrompt} from 'web/components/messaging/email-verification-prompt'
import {useFirebaseUser} from "web/hooks/use-firebase-user";
export const SendMessageButton = (props: {
toUser: User
@@ -27,7 +28,7 @@ export const SendMessageButton = (props: {
circleButton?: boolean
}) => {
const {toUser, currentUser, includeLabel, circleButton} = props
const firebaseUser = auth.currentUser
const firebaseUser = useFirebaseUser()
const router = useRouter()
const privateUser = usePrivateUser()
const channelMemberships = useSortedPrivateMessageMemberships(currentUser?.id)
@@ -132,7 +133,7 @@ export const SendMessageButton = (props: {
isSubmitting={!editor || submitting}
submitOnEnter={false}
/> :
<EmailVerificationPrompt firebaseUser={firebaseUser} t={t} className='max-w-xl'/>
<EmailVerificationPrompt t={t} className='max-w-xl'/>
}
<span className={'text-red-500'}>{error}</span>
</Col>

View File

@@ -0,0 +1,32 @@
import {useEffect, useState} from 'react'
import {onAuthStateChanged, onIdTokenChanged, User} from 'firebase/auth'
import {auth} from 'web/lib/firebase/users'
/**
* Subscribe to Firebase Auth user updates.
* Reactively returns the current Firebase `User` and updates when:
* - auth state changes (sign in/out)
* - ID token changes (after `getIdToken(true)` or `user.reload()`),
* which is important for reflecting `emailVerified` changes without a hard refresh.
*/
export function useFirebaseUser() {
const [, forceRender] = useState(0);
const [firebaseUser, setFirebaseUser] = useState<User | null>(auth.currentUser);
useEffect(() => {
const update = (u: User | null) => {
setFirebaseUser(u); // keep the real User instance
forceRender(v => v + 1); // force React to re-render
};
const unsubAuth = onAuthStateChanged(auth, update);
const unsubToken = onIdTokenChanged(auth, update);
return () => {
unsubAuth();
unsubToken();
};
}, []);
return firebaseUser;
}

View File

@@ -1,10 +1,9 @@
import toast from "react-hot-toast";
import {sendEmailVerification, User} from "firebase/auth";
import {auth} from "web/lib/firebase/users";
export const sendVerificationEmail = async (
user: User,
user: User | null,
t: any
) => {
// if (!privateUser?.email) {
@@ -15,6 +14,10 @@ export const sendVerificationEmail = async (
toast.error(t('settings.email.must_sign_in', 'You must be signed in to send a verification email.'))
return
}
if (user?.emailVerified) {
toast.success(t('settings.email.verified', 'Email Verified ✔️'))
return true
}
toast
.promise(sendEmailVerification(user), {
loading: t('settings.email.sending', 'Sending verification email...'),
@@ -33,7 +36,6 @@ export const sendVerificationEmail = async (
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const user = auth.currentUser;
if (!user) return false;
// Refresh user record from Firebase
@@ -41,9 +43,9 @@ export const sendVerificationEmail = async (
if (user.emailVerified) {
// IMPORTANT: force a new ID token with updated claims
await user.getIdToken(true);
await user.getIdToken(true)
toast.success(t('settings.email.verified', 'Email Verified ✔️'))
return true;
return true
}
await new Promise(r => setTimeout(r, intervalMs));

View File

@@ -22,15 +22,15 @@ import {BannedBadge} from 'web/components/widgets/user-link'
import {PrivateMessageChannel} from 'common/supabase/private-messages'
import {SEO} from "web/components/SEO";
import {useT} from 'web/lib/locale'
import {auth} from "web/lib/firebase/users";
import {EmailVerificationPrompt} from "web/components/messaging/email-verification-prompt";
import {useFirebaseUser} from 'web/hooks/use-firebase-user'
export default function MessagesPage() {
useRedirectIfSignedOut()
const currentUser = useUser()
const firebaseUser = auth.currentUser
const firebaseUser = useFirebaseUser()
const t = useT()
return (
<PageBase trackPageView={'messages page'} className={'p-2'}>
@@ -42,7 +42,7 @@ export default function MessagesPage() {
{currentUser && firebaseUser && (
firebaseUser?.emailVerified ?
<MessagesContent currentUser={currentUser}/>
: <EmailVerificationPrompt firebaseUser={firebaseUser} t={t}/>
: <EmailVerificationPrompt t={t}/>
)}
</PageBase>
)

View File

@@ -13,7 +13,6 @@ import {deleteAccount} from "web/lib/util/delete";
import router from "next/router";
import {Button} from "web/components/buttons/button";
import {updateEmail} from 'firebase/auth';
import {auth} from "web/lib/firebase/users";
import {NotificationSettings} from "web/components/notifications";
import ThemeIcon from "web/components/theme-icon";
import {WithPrivateUser} from "web/components/user/with-user";
@@ -26,6 +25,7 @@ import {EmailVerificationButton} from "web/components/email-verification-button"
import {api} from 'web/lib/api'
import {useUser} from "web/hooks/use-user";
import {isNativeMobile} from "web/lib/util/webview";
import {useFirebaseUser} from "web/hooks/use-firebase-user";
export default function NotificationsPage() {
const t = useT()
@@ -63,7 +63,7 @@ const LoadedGeneralSettings = (props: {
const {register, handleSubmit, formState: {errors}, reset} = useForm<{ newEmail: string }>()
const t = useT()
const user = auth.currentUser
const user = useFirebaseUser()
if (!user) return null
const handleDeleteAccount = async () => {
@@ -131,7 +131,7 @@ const LoadedGeneralSettings = (props: {
<h3>{t('settings.general.account', 'Account')}</h3>
<h5>{t('settings.general.email', 'Email')}</h5>
<EmailVerificationButton user={user}/>
<EmailVerificationButton/>
{!isChangingEmail ? (
<Button color={'gray-outline'} onClick={() => setIsChangingEmail(true)} className="w-fit">