Clean up verify state, handle normal callback

This commit is contained in:
Eric Bailey
2025-04-28 13:31:52 -05:00
parent c7e2e3dc29
commit 527945ad7f
3 changed files with 223 additions and 128 deletions
@@ -16,9 +16,9 @@ export function useInvalidateIsEmailVerified() {
} }
export function useIsEmailVerified({ export function useIsEmailVerified({
onEmailVerified, onVerify,
}: { }: {
onEmailVerified?: () => void onVerify?: () => void
} = {}) { } = {}) {
const agent = useAgent() const agent = useAgent()
const prevIsEmailVerified = useRef(!!agent.session?.emailConfirmed) const prevIsEmailVerified = useRef(!!agent.session?.emailConfirmed)
@@ -39,7 +39,7 @@ export function useIsEmailVerified({
if (query.data.isEmailVerified && !prevIsEmailVerified.current) { if (query.data.isEmailVerified && !prevIsEmailVerified.current) {
console.log('fire') console.log('fire')
prevIsEmailVerified.current = true prevIsEmailVerified.current = true
onEmailVerified?.() onVerify?.()
} else if (prevIsEmailVerified.current && !query.data.isEmailVerified) { } else if (prevIsEmailVerified.current && !query.data.isEmailVerified) {
console.log('reset') console.log('reset')
prevIsEmailVerified.current = false prevIsEmailVerified.current = false
@@ -89,6 +89,7 @@ function reducer(state: State, action: Action): State {
case 'setToken': { case 'setToken': {
return { return {
...state, ...state,
error: '',
token: action.value, token: action.value,
} }
} }
@@ -99,7 +100,6 @@ export function Update(_props: {config: Exclude<Screen, {id: 'Verify'}>}) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const [state, dispatch] = useReducer(reducer, { const [state, dispatch] = useReducer(reducer, {
step: 'email', step: 'email',
mutationStatus: 'default', mutationStatus: 'default',
@@ -1,4 +1,4 @@
import {useState} from 'react' import {useCallback, useReducer} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -22,93 +22,187 @@ import {createStaticClick, InlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {Span, Text} from '#/components/Typography' import {Span, Text} from '#/components/Typography'
type State = {
step: 'email' | 'token' | 'success'
mutationStatus: 'pending' | 'success' | 'error' | 'default'
error: string
token: string
}
type Action =
| {
type: 'setStep'
step: State['step']
}
| {
type: 'setError'
error: string
}
| {
type: 'setMutationStatus'
status: State['mutationStatus']
}
| {
type: 'setToken'
value: string
}
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'setStep': {
return {
...state,
error: '',
mutationStatus: 'default',
step: action.step,
}
}
case 'setError': {
return {
...state,
error: action.error,
mutationStatus: 'error',
}
}
case 'setMutationStatus': {
return {
...state,
error: '',
mutationStatus: action.status,
}
}
case 'setToken': {
return {
...state,
error: '',
token: action.value,
}
}
}
}
export function Verify({config}: {config: Extract<Screen, {id: 'Verify'}>}) { export function Verify({config}: {config: Extract<Screen, {id: 'Verify'}>}) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const [error, setError] = useState('') const [state, dispatch] = useReducer(reducer, {
step: 'email',
const [step, setStep] = useState< mutationStatus: 'default',
'default' | 'sent' | 'token' | 'tokenConfirmed' error: '',
>('default') token: '',
const [sendingStatus, setSendingStatus] = useState<'sending' | null>(null)
const {mutateAsync: requestEmailVerification} = useRequestEmailVerification()
const [token, setToken] = useState('')
const [confirmingStatus, setConfirmingStatus] = useState<'sending' | null>(
null,
)
const {mutateAsync: confirmEmail} = useConfirmEmail()
useIsEmailVerified({
onEmailVerified: () => {
config.onVerify?.()
},
}) })
const {mutateAsync: requestEmailVerification} = useRequestEmailVerification()
const {mutateAsync: confirmEmail} = useConfirmEmail()
const onVerify = useCallback(() => {
if (config.onVerify) {
config.onVerify()
} else {
dispatch({
type: 'setStep',
step: 'success',
})
}
}, [config, dispatch])
useIsEmailVerified({onVerify})
const handleRequestEmailVerification = async () => { const handleRequestEmailVerification = async () => {
setError('') dispatch({
setSendingStatus('sending') type: 'setMutationStatus',
status: 'pending',
})
try { try {
await wait(1000, requestEmailVerification()) await wait(1000, requestEmailVerification())
setStep('sent') dispatch({
type: 'setMutationStatus',
status: 'success',
})
} catch (e) { } catch (e) {
logger.error('EmailDialog: sending verification email failed', { logger.error('EmailDialog: sending verification email failed', {
safeMessage: e, safeMessage: e,
}) })
setError(_(msg`Failed to send email, please try again.`)) dispatch({
} finally { type: 'setError',
setSendingStatus(null) error: _(msg`Failed to send email, please try again.`),
})
} }
} }
const handleConfirmEmail = async () => { const handleConfirmEmail = async () => {
setError('') dispatch({
setConfirmingStatus('sending') type: 'setMutationStatus',
status: 'pending',
})
try { try {
await wait(1000, confirmEmail({token})) await wait(1000, confirmEmail({token: state.token}))
setStep('tokenConfirmed') dispatch({
type: 'setStep',
step: 'success',
})
} catch (e) { } catch (e) {
logger.error('EmailDialog: confirming email failed', { logger.error('EmailDialog: confirming email failed', {
safeMessage: e, safeMessage: e,
}) })
setError(_(msg`Failed to verify email, please try again.`)) dispatch({
} finally { type: 'setError',
setConfirmingStatus(null) error: _(msg`Failed to verify email, please try again.`),
})
} }
} }
if (state.step === 'success') {
return (
<View style={[a.gap_lg]}>
<View style={[a.gap_sm]}>
<Text style={[a.text_xl, a.font_heavy]}>
<Span style={{top: 1}}>
<Check size="sm" fill={t.palette.positive_500} />
</Span>{' '}
<Trans>Email verification complete!</Trans>
</Text>
<Text
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans>
You have successfully verified your email address. You can close
this dialog.
</Trans>
</Text>
</View>
</View>
)
}
return ( return (
<View style={[a.gap_lg]}> <View style={[a.gap_lg]}>
<View style={[a.gap_sm]}> <View style={[a.gap_sm]}>
<Text style={[a.text_xl, a.font_heavy]}> <Text style={[a.text_xl, a.font_heavy]}>
{step === 'sent' ? ( {state.step === 'email' ? (
state.mutationStatus === 'success' ? (
<> <>
<Span style={{top: 1}}> <Span style={{top: 1}}>
<Check size="sm" fill={t.palette.positive_500} /> <Check size="sm" fill={t.palette.positive_500} />
</Span>{' '} </Span>{' '}
<Trans>Email sent!</Trans> <Trans>Email sent!</Trans>
</> </>
) : step === 'tokenConfirmed' ? (
<>
<Span style={{top: 1}}>
<Check size="sm" fill={t.palette.positive_500} />
</Span>{' '}
<Trans>Email verification complete</Trans>
</>
) : ( ) : (
<Trans>Verify email</Trans> <Trans>Verify email</Trans>
)
) : (
<Trans>Confirm email code</Trans>
)} )}
</Text> </Text>
{step === 'default' && ( {state.step === 'email' && state.mutationStatus !== 'success' && (
<> <>
{config.instructions?.map((int, i) => ( {config.instructions?.map((int, i) => (
<Text <Text
key={i} key={i}
style={[ style={[
a.italic,
a.text_sm, a.text_sm,
a.leading_snug, a.leading_snug,
t.atoms.text_contrast_medium, t.atoms.text_contrast_medium,
@@ -120,27 +214,15 @@ export function Verify({config}: {config: Extract<Screen, {id: 'Verify'}>}) {
)} )}
<Text style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}> <Text style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
{step === 'sent' ? ( {state.step === 'email' ? (
state.mutationStatus === 'success' ? (
<Trans> <Trans>
We sent an email to{' '} We sent an email to{' '}
<Span style={[a.font_bold, t.atoms.text]}> <Span style={[a.font_bold, t.atoms.text]}>
{currentAccount!.email} {currentAccount!.email}
</Span>{' '} </Span>{' '}
containing a link. Click on it to complete the email verification containing a link. Click on it to complete the email
process. verification process.
</Trans>
) : step === 'token' ? (
<Trans>
Enter the code we sent to{' '}
<Span style={[a.font_bold, t.atoms.text]}>
{currentAccount!.email}
</Span>{' '}
below.
</Trans>
) : step === 'tokenConfirmed' ? (
<Trans>
You have successfully verified your email address. You can close
this dialog.
</Trans> </Trans>
) : ( ) : (
<Trans> <Trans>
@@ -148,18 +230,47 @@ export function Verify({config}: {config: Extract<Screen, {id: 'Verify'}>}) {
<Span style={[a.font_bold, t.atoms.text]}> <Span style={[a.font_bold, t.atoms.text]}>
{currentAccount!.email} {currentAccount!.email}
</Span>{' '} </Span>{' '}
containing a link. Click on it to complete the email verification containing a link. Click on it to complete the email
process. verification process.
</Trans>
)
) : (
<Trans>
Enter the code we sent to{' '}
<Span style={[a.font_bold, t.atoms.text]}>
{currentAccount!.email}
</Span>{' '}
below.
</Trans> </Trans>
)} )}
</Text> </Text>
{step === 'sent' && ( {state.step === 'email' && state.mutationStatus === 'success' && (
<ResendEmailText onPress={requestEmailVerification} /> <ResendEmailText onPress={requestEmailVerification} />
)} )}
</View> </View>
{step === 'sent' && ( {state.step === 'email' && state.mutationStatus !== 'success' ? (
<>
{state.error && <Admonition type="error">{state.error}</Admonition>}
<Button
label={_(msg`Send verification email`)}
size="large"
variant="solid"
color="primary"
onPress={handleRequestEmailVerification}
disabled={state.mutationStatus === 'pending'}>
<ButtonText>
<Trans>Send email</Trans>
</ButtonText>
<ButtonIcon
icon={state.mutationStatus === 'pending' ? Loader : Envelope}
/>
</Button>
</>
) : null}
{state.step === 'email' && (
<> <>
<Divider /> <Divider />
@@ -170,7 +281,10 @@ export function Verify({config}: {config: Extract<Screen, {id: 'Verify'}>}) {
<InlineLinkText <InlineLinkText
label={_(msg`Enter code`)} label={_(msg`Enter code`)}
{...createStaticClick(() => { {...createStaticClick(() => {
setStep('token') dispatch({
type: 'setStep',
step: 'token',
})
})}> })}>
Click here. Click here.
</InlineLinkText> </InlineLinkText>
@@ -179,58 +293,20 @@ export function Verify({config}: {config: Extract<Screen, {id: 'Verify'}>}) {
</> </>
)} )}
{step === 'default' ? ( {state.step === 'token' ? (
<>
{error && <Admonition type="error"> {error} </Admonition>}
<Button
label={_(msg`Send verification email`)}
size="large"
variant="solid"
color="primary"
onPress={handleRequestEmailVerification}
disabled={sendingStatus === 'sending'}>
<ButtonText>
<Trans>Send email</Trans>
</ButtonText>
<ButtonIcon
icon={sendingStatus === 'sending' ? Loader : Envelope}
/>
</Button>
{!config.hideInitialCodeButton && (
<>
<Divider />
<Text
style={[
a.text_sm,
a.leading_snug,
t.atoms.text_contrast_medium,
]}>
<Trans>
Have a code?{' '}
<InlineLinkText
label={_(msg`Enter code`)}
{...createStaticClick(() => {
setStep('token')
})}>
Click here.
</InlineLinkText>
</Trans>
</Text>
</>
)}
</>
) : step === 'token' ? (
<> <>
<TokenField <TokenField
value={token} value={state.token}
onChangeText={setToken} onChangeText={token => {
dispatch({
type: 'setToken',
value: token,
})
}}
onSubmitEditing={() => {}} onSubmitEditing={() => {}}
/> />
{error && <Admonition type="error"> {error} </Admonition>} {state.error && <Admonition type="error">{state.error}</Admonition>}
<Button <Button
label={_(msg`Verify`)} label={_(msg`Verify`)}
@@ -238,12 +314,31 @@ export function Verify({config}: {config: Extract<Screen, {id: 'Verify'}>}) {
variant="solid" variant="solid"
color="primary" color="primary"
onPress={handleConfirmEmail} onPress={handleConfirmEmail}
disabled={!token || confirmingStatus === 'sending'}> disabled={!state.token || state.mutationStatus === 'pending'}>
<ButtonText> <ButtonText>
<Trans>Verify code</Trans> <Trans>Verify code</Trans>
</ButtonText> </ButtonText>
{confirmingStatus === 'sending' && <ButtonIcon icon={Loader} />} {state.mutationStatus === 'pending' && <ButtonIcon icon={Loader} />}
</Button> </Button>
<Divider />
<Text
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans>
Don't have a code?{' '}
<InlineLinkText
label={_(msg`Enter code`)}
{...createStaticClick(() => {
dispatch({
type: 'setStep',
step: 'email',
})
})}>
Click here.
</InlineLinkText>
</Trans>
</Text>
</> </>
) : null} ) : null}
</View> </View>