[APP-1158] Refactor email-related dialogs (#8296)

* WIP

* Update email

* Fire off confirmation email after change

* Verify step, integrate stateful control

* Remove tentative EnterCode step

* Handle token step

* Handle instructions and integrate into 2FA setting

* Fix load state when reusing same email

* Add new state

* Add 2FA screens

* Clean up state in Update step

* Clean up verify state, handle normal callback

* Normalize convetions

* Add verification reminder screen

* Improve session refresh

* Handle verification requirements for composer and convo

* Fix lint

* Do better

* Couple missing translations

* Format

* Use listeners for easier to grok logic

* Clean errors

* Move to global context

* [APP-1158] Gate features by email verification state (#8305)

* Use new hook in all locations

* Format

* Seems to work, not great duplication

* Wrap all open composer calls

* Remove unneeded spans

* Missed one

* Fix handler on Conversation

* Gate new chat in header

* Add comment

* Remove whoopsie

* Format

* add hackfix for dialog not showing

* add prompt to accept chat btn

* navigation not necessary

* send back one screen, rather than home

* Update comment

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

* Clear dialog state

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

* Update icon

* Check color

* Add 2FA warning

* Update instructions

* Fix X button

* Use an effect silly goose

* Update copy

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update copy

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update copy

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update copy

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update src/components/dialaUpdate copyogs/EmailDialog/screens/Update.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update copy

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update copy

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update copy

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update copy

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update copy

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update copy

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update copy

* Update copy

* Update copy

* Update copy

* Update copy

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update copy

* Add link back to update email from verify email dialog

* Handle token field validation

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
This commit is contained in:
Eric Bailey
2025-05-07 10:23:33 -05:00
committed by GitHub
parent 0edd3bd3b4
commit 0f96669f8c
55 changed files with 2257 additions and 402 deletions
@@ -0,0 +1,386 @@
import {useReducer} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {wait} from '#/lib/async/wait'
import {useCleanError} from '#/lib/hooks/useCleanError'
import {logger} from '#/logger'
import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {ResendEmailText} from '#/components/dialogs/EmailDialog/components/ResendEmailText'
import {
isValidCode,
TokenField,
} from '#/components/dialogs/EmailDialog/components/TokenField'
import {useConfirmEmail} from '#/components/dialogs/EmailDialog/data/useConfirmEmail'
import {useRequestEmailVerification} from '#/components/dialogs/EmailDialog/data/useRequestEmailVerification'
import {useOnEmailVerified} from '#/components/dialogs/EmailDialog/events'
import {
ScreenID,
type ScreenProps,
} from '#/components/dialogs/EmailDialog/types'
import {Divider} from '#/components/Divider'
import {CheckThick_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope'
import {createStaticClick, InlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader'
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, showScreen}: ScreenProps<ScreenID.Verify>) {
const t = useTheme()
const {_} = useLingui()
const cleanError = useCleanError()
const {currentAccount} = useSession()
const [state, dispatch] = useReducer(reducer, {
step: 'email',
mutationStatus: 'default',
error: '',
token: '',
})
const {mutateAsync: requestEmailVerification} = useRequestEmailVerification()
const {mutateAsync: confirmEmail} = useConfirmEmail()
useOnEmailVerified(() => {
if (config.onVerify) {
config.onVerify()
} else {
dispatch({
type: 'setStep',
step: 'success',
})
}
})
const handleRequestEmailVerification = async () => {
dispatch({
type: 'setMutationStatus',
status: 'pending',
})
try {
await wait(1000, requestEmailVerification())
dispatch({
type: 'setMutationStatus',
status: 'success',
})
} catch (e) {
logger.error('EmailDialog: sending verification email failed', {
safeMessage: e,
})
const {clean} = cleanError(e)
dispatch({
type: 'setError',
error: clean || _(msg`Failed to send email, please try again.`),
})
}
}
const handleConfirmEmail = async () => {
if (!isValidCode(state.token)) {
dispatch({
type: 'setError',
error: _(msg`Please enter a valid code.`),
})
return
}
dispatch({
type: 'setMutationStatus',
status: 'pending',
})
try {
await wait(1000, confirmEmail({token: state.token}))
dispatch({
type: 'setStep',
step: 'success',
})
} catch (e) {
logger.error('EmailDialog: confirming email failed', {
safeMessage: e,
})
const {clean} = cleanError(e)
dispatch({
type: 'setError',
error: clean || _(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_600} />
</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 (
<View style={[a.gap_lg]}>
<View style={[a.gap_sm]}>
<Text style={[a.text_xl, a.font_heavy]}>
{state.step === 'email' ? (
state.mutationStatus === 'success' ? (
<>
<Span style={{top: 1}}>
<Check size="sm" fill={t.palette.positive_600} />
</Span>
{' '}
<Trans>Email sent!</Trans>
</>
) : (
<Trans>Verify your email</Trans>
)
) : (
<Trans>Verify email code</Trans>
)}
</Text>
{state.step === 'email' && state.mutationStatus !== 'success' && (
<>
{config.instructions?.map((int, i) => (
<Text
key={i}
style={[
a.italic,
a.text_sm,
a.leading_snug,
t.atoms.text_contrast_medium,
]}>
{int}
</Text>
))}
</>
)}
<Text style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
{state.step === 'email' ? (
state.mutationStatus === 'success' ? (
<Trans>
We sent an email to{' '}
<Span style={[a.font_bold, t.atoms.text]}>
{currentAccount!.email}
</Span>{' '}
containing a link. Please click on it to complete the email
verification process.
</Trans>
) : (
<Trans>
We'll send an email to{' '}
<Span style={[a.font_bold, t.atoms.text]}>
{currentAccount!.email}
</Span>{' '}
containing a link. Please click on it to complete the email
verification process.
</Trans>
)
) : (
<Trans>
Please enter the code we sent to{' '}
<Span style={[a.font_bold, t.atoms.text]}>
{currentAccount!.email}
</Span>{' '}
below.
</Trans>
)}
</Text>
{state.step === 'email' && state.mutationStatus !== 'success' && (
<Text
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans>
If you need to update your email,{' '}
<InlineLinkText
label={_(msg`Click here to update your email`)}
{...createStaticClick(() => {
showScreen({id: ScreenID.Update})
})}>
click here
</InlineLinkText>
.
</Trans>
</Text>
)}
{state.step === 'email' && state.mutationStatus === 'success' && (
<ResendEmailText onPress={requestEmailVerification} />
)}
</View>
{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 />
<Text
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans>
Have a code?{' '}
<InlineLinkText
label={_(msg`Enter code`)}
{...createStaticClick(() => {
dispatch({
type: 'setStep',
step: 'token',
})
})}>
Click here.
</InlineLinkText>
</Trans>
</Text>
</>
)}
{state.step === 'token' ? (
<>
<TokenField
value={state.token}
onChangeText={token => {
dispatch({
type: 'setToken',
value: token,
})
}}
onSubmitEditing={handleConfirmEmail}
/>
{state.error && <Admonition type="error">{state.error}</Admonition>}
<Button
label={_(msg`Verify code`)}
size="large"
variant="solid"
color="primary"
onPress={handleConfirmEmail}
disabled={
!state.token ||
state.token.length !== 11 ||
state.mutationStatus === 'pending'
}>
<ButtonText>
<Trans>Verify code</Trans>
</ButtonText>
{state.mutationStatus === 'pending' && <ButtonIcon icon={Loader} />}
</Button>
<Divider />
<Text
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans>
Don't have a code or need a new one?{' '}
<InlineLinkText
label={_(msg`Click here to restart the verification process.`)}
{...createStaticClick(() => {
dispatch({
type: 'setStep',
step: 'email',
})
})}>
Click here.
</InlineLinkText>
</Trans>
</Text>
</>
) : null}
</View>
)
}