diff --git a/web/components/email-verification-button.tsx b/web/components/email-verification-button.tsx index a21c5f0..bdd6b50 100644 --- a/web/components/email-verification-button.tsx +++ b/web/components/email-verification-button.tsx @@ -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 ( - + +
+ +
) } diff --git a/web/components/messaging/email-verification-prompt.tsx b/web/components/messaging/email-verification-prompt.tsx index 8675641..3d2d7b8 100644 --- a/web/components/messaging/email-verification-prompt.tsx +++ b/web/components/messaging/email-verification-prompt.tsx @@ -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 (

{t('messaging.email_verification_required', "You must verify your email to message people.")}

- + ) } diff --git a/web/components/messaging/send-message-button.tsx b/web/components/messaging/send-message-button.tsx index 1c2bd50..3ef9cf1 100644 --- a/web/components/messaging/send-message-button.tsx +++ b/web/components/messaging/send-message-button.tsx @@ -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} /> : - + } {error} diff --git a/web/hooks/use-firebase-user.ts b/web/hooks/use-firebase-user.ts new file mode 100644 index 0000000..769f862 --- /dev/null +++ b/web/hooks/use-firebase-user.ts @@ -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(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; +} diff --git a/web/lib/firebase/email-verification.ts b/web/lib/firebase/email-verification.ts index 82c5b64..ce57e60 100644 --- a/web/lib/firebase/email-verification.ts +++ b/web/lib/firebase/email-verification.ts @@ -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)); diff --git a/web/pages/messages/index.tsx b/web/pages/messages/index.tsx index d802e2b..4e690d9 100644 --- a/web/pages/messages/index.tsx +++ b/web/pages/messages/index.tsx @@ -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 ( @@ -42,7 +42,7 @@ export default function MessagesPage() { {currentUser && firebaseUser && ( firebaseUser?.emailVerified ? - : + : )} ) diff --git a/web/pages/settings.tsx b/web/pages/settings.tsx index 1b65ccc..0aec2da 100644 --- a/web/pages/settings.tsx +++ b/web/pages/settings.tsx @@ -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: {

{t('settings.general.account', 'Account')}

{t('settings.general.email', 'Email')}
- + {!isChangingEmail ? (