From f11b24e80e97b177b6f7fb476d19d21fe55cf5a2 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 28 Apr 2025 20:38:55 -0500 Subject: [PATCH] Use listeners for easier to grok logic --- .../EmailDialog/data/useAccountEmailState.ts | 41 +++++++++++-------- src/components/dialogs/EmailDialog/events.ts | 23 +++++++++++ .../dialogs/EmailDialog/screens/Update.tsx | 1 + .../dialogs/EmailDialog/screens/Verify.tsx | 9 ++-- 4 files changed, 51 insertions(+), 23 deletions(-) create mode 100644 src/components/dialogs/EmailDialog/events.ts diff --git a/src/components/dialogs/EmailDialog/data/useAccountEmailState.ts b/src/components/dialogs/EmailDialog/data/useAccountEmailState.ts index 684e05e0c7..377411107a 100644 --- a/src/components/dialogs/EmailDialog/data/useAccountEmailState.ts +++ b/src/components/dialogs/EmailDialog/data/useAccountEmailState.ts @@ -1,14 +1,15 @@ -import {useCallback, useState} from 'react' +import {useCallback, useEffect, useState} from 'react' import {useQuery, useQueryClient} from '@tanstack/react-query' import {useAgent} from '#/state/session' +import {emitEmailVerified} from '#/components/dialogs/EmailDialog/events' export type AccountEmailState = { isEmailVerified: boolean email2FAEnabled: boolean } -export const accountEmailStateQueryKey = ['isEmailVerified'] as const +export const accountEmailStateQueryKey = ['accountEmailState'] as const export function useInvalidateAccountEmailState() { const qc = useQueryClient() @@ -36,21 +37,17 @@ export function useUpdateAccountEmailStateQueryCache() { ) } -export function useAccountEmailState({ - onVerify, -}: { - onVerify?: () => void -} = {}) { +export function useAccountEmailState() { const agent = useAgent() const [prevIsEmailVerified, setPrevEmailIsVerified] = useState( !!agent.session?.emailConfirmed, ) + const fallbackData: AccountEmailState = { + isEmailVerified: !!agent.session?.emailConfirmed, + email2FAEnabled: !!agent.session?.emailAuthFactor, + } const query = useQuery({ enabled: !!agent.session, - initialData: { - isEmailVerified: !!agent.session?.emailConfirmed, - email2FAEnabled: !!agent.session?.emailAuthFactor, - }, refetchOnWindowFocus: true, queryKey: accountEmailStateQueryKey, queryFn: async () => { @@ -63,12 +60,20 @@ export function useAccountEmailState({ }, }) - if (query.data.isEmailVerified && !prevIsEmailVerified) { - setPrevEmailIsVerified(true) - onVerify?.() - } else if (!query.data.isEmailVerified && prevIsEmailVerified) { - setPrevEmailIsVerified(false) - } + const state = query.data ?? fallbackData - return query.data + /* + * This will emit `n` times for each instance of this hook. So the listeners + * all use `once` to prevent multiple handlers firing. + */ + useEffect(() => { + if (state.isEmailVerified && !prevIsEmailVerified) { + setPrevEmailIsVerified(true) + emitEmailVerified() + } else if (!state.isEmailVerified && prevIsEmailVerified) { + setPrevEmailIsVerified(false) + } + }, [state, prevIsEmailVerified]) + + return state } diff --git a/src/components/dialogs/EmailDialog/events.ts b/src/components/dialogs/EmailDialog/events.ts new file mode 100644 index 0000000000..4fa171cad5 --- /dev/null +++ b/src/components/dialogs/EmailDialog/events.ts @@ -0,0 +1,23 @@ +import {useEffect} from 'react' +import EventEmitter from 'eventemitter3' + +const events = new EventEmitter<{ + emailVerified: void +}>() + +export function emitEmailVerified() { + events.emit('emailVerified') +} + +export function useOnEmailVerified(cb: () => void) { + useEffect(() => { + /* + * N.B. Use `once` here, since the event can fire multiple times for each + * instance of `useAccountEmailState` + */ + events.once('emailVerified', cb) + return () => { + events.off('emailVerified', cb) + } + }, [cb]) +} diff --git a/src/components/dialogs/EmailDialog/screens/Update.tsx b/src/components/dialogs/EmailDialog/screens/Update.tsx index 592372f895..fb3ee39ed1 100644 --- a/src/components/dialogs/EmailDialog/screens/Update.tsx +++ b/src/components/dialogs/EmailDialog/screens/Update.tsx @@ -207,6 +207,7 @@ export function Update(_props: ScreenProps) { } keyboardType="email-address" autoComplete="email" + autoCapitalize="none" onSubmitEditing={handleUpdateEmail} /> diff --git a/src/components/dialogs/EmailDialog/screens/Verify.tsx b/src/components/dialogs/EmailDialog/screens/Verify.tsx index 577c5b8f2c..2eec7dbf8e 100644 --- a/src/components/dialogs/EmailDialog/screens/Verify.tsx +++ b/src/components/dialogs/EmailDialog/screens/Verify.tsx @@ -1,4 +1,4 @@ -import {useCallback, useReducer} from 'react' +import {useReducer} from 'react' import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -11,9 +11,9 @@ import {Admonition} from '#/components/Admonition' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {ResendEmailText} from '#/components/dialogs/EmailDialog/components/ResendEmailText' import {TokenField} from '#/components/dialogs/EmailDialog/components/TokenField' -import {useAccountEmailState} from '#/components/dialogs/EmailDialog/data/useAccountEmailState' import {useConfirmEmail} from '#/components/dialogs/EmailDialog/data/useConfirmEmail' import {useRequestEmailVerification} from '#/components/dialogs/EmailDialog/data/useRequestEmailVerification' +import {useOnEmailVerified} from '#/components/dialogs/EmailDialog/events' import { type ScreenID, type ScreenProps, @@ -98,7 +98,7 @@ export function Verify({config}: ScreenProps) { const {mutateAsync: requestEmailVerification} = useRequestEmailVerification() const {mutateAsync: confirmEmail} = useConfirmEmail() - const onVerify = useCallback(() => { + useOnEmailVerified(() => { if (config.onVerify) { config.onVerify() } else { @@ -107,8 +107,7 @@ export function Verify({config}: ScreenProps) { step: 'success', }) } - }, [config, dispatch]) - useAccountEmailState({onVerify}) + }) const handleRequestEmailVerification = async () => { dispatch({