[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:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user