Add 2FA screens
This commit is contained in:
@@ -1,10 +1,12 @@
|
|||||||
import {useMutation} from '@tanstack/react-query'
|
import {useMutation} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
|
import {useInvalidateIsEmailVerified} from '#/components/dialogs/EmailDialog/data/useIsEmailVerified'
|
||||||
|
|
||||||
export function useConfirmEmail() {
|
export function useConfirmEmail() {
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
const invalidateIsEmailVerified = useInvalidateIsEmailVerified()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({token}: {token: string}) => {
|
mutationFn: async ({token}: {token: string}) => {
|
||||||
@@ -17,6 +19,7 @@ export function useConfirmEmail() {
|
|||||||
token: token.trim(),
|
token: token.trim(),
|
||||||
})
|
})
|
||||||
await agent.resumeSession(agent.session!)
|
await agent.resumeSession(agent.session!)
|
||||||
|
await invalidateIsEmailVerified()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import {useCallback, useRef} from 'react'
|
||||||
|
import {useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
|
export const isEmailVerifiedQueryKey = ['isEmailVerified'] as const
|
||||||
|
|
||||||
|
export function useInvalidateIsEmailVerified() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
|
||||||
|
return useCallback(() => {
|
||||||
|
return qc.invalidateQueries({
|
||||||
|
queryKey: isEmailVerifiedQueryKey,
|
||||||
|
})
|
||||||
|
}, [qc])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useIsEmailVerified({
|
||||||
|
onEmailVerified,
|
||||||
|
}: {
|
||||||
|
onEmailVerified?: () => void
|
||||||
|
} = {}) {
|
||||||
|
const agent = useAgent()
|
||||||
|
const prevIsEmailVerified = useRef(!!agent.session?.emailConfirmed)
|
||||||
|
const query = useQuery({
|
||||||
|
enabled: !!agent.session,
|
||||||
|
initialData: {isEmailVerified: !!agent.session?.emailConfirmed},
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
queryKey: isEmailVerifiedQueryKey,
|
||||||
|
queryFn: async () => {
|
||||||
|
const {data} = await agent.com.atproto.server.getSession()
|
||||||
|
return {
|
||||||
|
isEmailVerified: !!data.emailConfirmed,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO double check racing?
|
||||||
|
if (query.data.isEmailVerified && !prevIsEmailVerified.current) {
|
||||||
|
console.log('fire')
|
||||||
|
prevIsEmailVerified.current = true
|
||||||
|
onEmailVerified?.()
|
||||||
|
} else if (prevIsEmailVerified.current && !query.data.isEmailVerified) {
|
||||||
|
console.log('reset')
|
||||||
|
prevIsEmailVerified.current = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.data
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import {useMutation} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {useAgent, useSession} from '#/state/session'
|
||||||
|
|
||||||
|
export function useManageEmail2FA() {
|
||||||
|
const agent = useAgent()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
enabled,
|
||||||
|
token,
|
||||||
|
}:
|
||||||
|
| {enabled: true; token?: undefined}
|
||||||
|
| {enabled: false; token: string}) => {
|
||||||
|
if (!currentAccount?.email) {
|
||||||
|
throw new Error('No email found for the current account')
|
||||||
|
}
|
||||||
|
|
||||||
|
await agent.com.atproto.server.updateEmail({
|
||||||
|
email: currentAccount.email,
|
||||||
|
emailAuthFactor: enabled,
|
||||||
|
token,
|
||||||
|
})
|
||||||
|
await agent.resumeSession(agent.session!)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {useCallback} from 'react'
|
import {useCallback, useState} from 'react'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
useStatefulDialogControl,
|
useStatefulDialogControl,
|
||||||
} from '#/components/dialogs/Context'
|
} from '#/components/dialogs/Context'
|
||||||
import {useRefreshSession} from '#/components/dialogs/EmailDialog/data/useRefreshSession'
|
import {useRefreshSession} from '#/components/dialogs/EmailDialog/data/useRefreshSession'
|
||||||
|
import {Manage2FA} from '#/components/dialogs/EmailDialog/screens/Manage2FA'
|
||||||
/*
|
/*
|
||||||
* Steps
|
* Steps
|
||||||
*/
|
*/
|
||||||
@@ -50,14 +51,19 @@ export function EmailDialog({control}: {control: StatefulControl<Screen>}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Inner({control}: {control: StatefulControl<Screen>}) {
|
function Inner({control}: {control: StatefulControl<Screen>}) {
|
||||||
if (!control.value) return null
|
const [screen, showScreen] = useState(() => control.value)
|
||||||
|
|
||||||
switch (control.value.id) {
|
if (!screen) return null
|
||||||
|
|
||||||
|
switch (screen.id) {
|
||||||
case ScreenID.Update: {
|
case ScreenID.Update: {
|
||||||
return <Update config={control.value} />
|
return <Update config={screen} />
|
||||||
}
|
}
|
||||||
case ScreenID.Verify: {
|
case ScreenID.Verify: {
|
||||||
return <Verify config={control.value} />
|
return <Verify config={screen} />
|
||||||
|
}
|
||||||
|
case ScreenID.Manage2FA: {
|
||||||
|
return <Manage2FA config={screen} showScreen={showScreen} />
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
import {useReducer, useState} 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 {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 {useDialogContext} from '#/components/Dialog'
|
||||||
|
import {ResendEmailText} from '#/components/dialogs/EmailDialog/components/ResendEmailText'
|
||||||
|
import {TokenField} from '#/components/dialogs/EmailDialog/components/TokenField'
|
||||||
|
import {useManageEmail2FA} from '#/components/dialogs/EmailDialog/data/useManageEmail2FA'
|
||||||
|
import {useRequestEmailUpdate} from '#/components/dialogs/EmailDialog/data/useRequestEmailUpdate'
|
||||||
|
import {Divider} from '#/components/Divider'
|
||||||
|
import {Check_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 = {
|
||||||
|
error: string
|
||||||
|
stage: 'email' | 'token'
|
||||||
|
emailStatus: 'pending' | 'success' | 'error' | 'default'
|
||||||
|
tokenStatus: 'pending' | 'success' | 'error' | 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| {
|
||||||
|
id: 'setError'
|
||||||
|
error: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
id: 'setStage'
|
||||||
|
stage: 'email' | 'token'
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
id: 'setEmailStatus'
|
||||||
|
status: State['emailStatus']
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
id: 'setTokenStatus'
|
||||||
|
status: State['tokenStatus']
|
||||||
|
}
|
||||||
|
|
||||||
|
function reducer(state: State, action: Action): State {
|
||||||
|
switch (action.id) {
|
||||||
|
case 'setError': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: action.error,
|
||||||
|
emailStatus: 'error',
|
||||||
|
tokenStatus: 'error',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'setStage': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: '',
|
||||||
|
stage: action.stage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'setEmailStatus': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: '',
|
||||||
|
emailStatus: action.status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'setTokenStatus': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: '',
|
||||||
|
tokenStatus: action.status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Disable() {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
const {mutateAsync: requestEmailUpdate} = useRequestEmailUpdate()
|
||||||
|
const {mutateAsync: manageEmail2FA} = useManageEmail2FA()
|
||||||
|
const control = useDialogContext()
|
||||||
|
|
||||||
|
const [token, setToken] = useState('')
|
||||||
|
const [state, dispatch] = useReducer(reducer, {
|
||||||
|
error: '',
|
||||||
|
stage: 'email',
|
||||||
|
emailStatus: 'default',
|
||||||
|
tokenStatus: 'default',
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSendEmail = async () => {
|
||||||
|
dispatch({id: 'setEmailStatus', status: 'pending'})
|
||||||
|
try {
|
||||||
|
await wait(1000, requestEmailUpdate())
|
||||||
|
dispatch({id: 'setEmailStatus', status: 'success'})
|
||||||
|
setTimeout(() => {
|
||||||
|
dispatch({id: 'setStage', stage: 'token'})
|
||||||
|
}, 1000)
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Manage2FA: email update code request failed', {
|
||||||
|
safeMessage: e,
|
||||||
|
})
|
||||||
|
// TODO rate limit
|
||||||
|
dispatch({
|
||||||
|
id: 'setError',
|
||||||
|
error: _(msg`Failed to send email, please try again.`),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleManageEmail2FA = async () => {
|
||||||
|
dispatch({id: 'setTokenStatus', status: 'pending'})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await wait(1000, manageEmail2FA({enabled: false, token}))
|
||||||
|
dispatch({id: 'setTokenStatus', status: 'success'})
|
||||||
|
setTimeout(() => {
|
||||||
|
control.close()
|
||||||
|
}, 1000)
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Manage2FA: disable email 2FA failed', {safeMessage: e})
|
||||||
|
dispatch({
|
||||||
|
id: 'setError',
|
||||||
|
error: _(msg`Update to email 2FA settings failed`),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.gap_sm]}>
|
||||||
|
<Text style={[a.text_xl, a.font_heavy, a.leading_snug]}>
|
||||||
|
<Trans>Disable email 2FA</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{state.stage === 'email' ? (
|
||||||
|
<>
|
||||||
|
<Text
|
||||||
|
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>
|
||||||
|
To disable your email 2FA method, please verify your access to{' '}
|
||||||
|
<Span style={[a.font_bold]}>{currentAccount?.email}</Span>
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={[a.gap_lg, a.pt_sm]}>
|
||||||
|
{state.error && <Admonition type="error">{state.error}</Admonition>}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label={_(msg`Send email`)}
|
||||||
|
size="large"
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
onPress={handleSendEmail}
|
||||||
|
disabled={state.emailStatus === 'pending'}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Send email</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
<ButtonIcon
|
||||||
|
icon={
|
||||||
|
state.emailStatus === 'pending'
|
||||||
|
? Loader
|
||||||
|
: state.emailStatus === 'success'
|
||||||
|
? Check
|
||||||
|
: Envelope
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<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({id: 'setStage', stage: 'token'})
|
||||||
|
})}>
|
||||||
|
Click here.
|
||||||
|
</InlineLinkText>
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text
|
||||||
|
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>
|
||||||
|
To disable your email 2FA method, please verify your access to{' '}
|
||||||
|
<Span style={[a.font_bold]}>{currentAccount?.email}</Span>
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={[a.gap_sm, a.py_sm]}>
|
||||||
|
<TokenField
|
||||||
|
value={token}
|
||||||
|
onChangeText={setToken}
|
||||||
|
onSubmitEditing={() => {}}
|
||||||
|
/>
|
||||||
|
<ResendEmailText onPress={handleSendEmail} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{state.error && <Admonition type="error">{state.error}</Admonition>}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label={_(msg`Disable 2FAVerify`)}
|
||||||
|
size="large"
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
onPress={handleManageEmail2FA}
|
||||||
|
disabled={!token || state.tokenStatus === 'pending'}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Disable 2FA</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
{state.tokenStatus === 'pending' ? (
|
||||||
|
<ButtonIcon icon={Loader} />
|
||||||
|
) : state.tokenStatus === 'success' ? (
|
||||||
|
<ButtonIcon icon={Loader} />
|
||||||
|
) : null}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
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 {logger} from '#/logger'
|
||||||
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
|
import {Admonition} from '#/components/Admonition'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import {useDialogContext} from '#/components/Dialog'
|
||||||
|
import {useManageEmail2FA} from '#/components/dialogs/EmailDialog/data/useManageEmail2FA'
|
||||||
|
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||||
|
import {ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon} from '#/components/icons/Shield'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
error: string
|
||||||
|
status: 'pending' | 'success' | 'error' | 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| {
|
||||||
|
id: 'setError'
|
||||||
|
error: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
id: 'setStatus'
|
||||||
|
status: State['status']
|
||||||
|
}
|
||||||
|
|
||||||
|
function reducer(state: State, action: Action): State {
|
||||||
|
switch (action.id) {
|
||||||
|
case 'setError': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: action.error,
|
||||||
|
status: 'error',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'setStatus': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: '',
|
||||||
|
status: action.status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Enable() {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {gtPhone} = useBreakpoints()
|
||||||
|
const {mutateAsync: manageEmail2FA} = useManageEmail2FA()
|
||||||
|
const control = useDialogContext()
|
||||||
|
|
||||||
|
const [state, dispatch] = useReducer(reducer, {
|
||||||
|
error: '',
|
||||||
|
status: 'default',
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleManageEmail2FA = async () => {
|
||||||
|
dispatch({id: 'setStatus', status: 'pending'})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await wait(1000, manageEmail2FA({enabled: true}))
|
||||||
|
dispatch({id: 'setStatus', status: 'success'})
|
||||||
|
setTimeout(() => {
|
||||||
|
control.close()
|
||||||
|
}, 1000)
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Manage2FA: enable email 2FA failed', {safeMessage: e})
|
||||||
|
dispatch({
|
||||||
|
id: 'setError',
|
||||||
|
error: _(msg`Update to email 2FA settings failed`),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.gap_lg]}>
|
||||||
|
<View style={[a.gap_sm]}>
|
||||||
|
<Text style={[a.text_xl, a.font_heavy, a.leading_snug]}>
|
||||||
|
<Trans>Enable email 2FA</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>Require an email code to sign in to your account.</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{state.error && <Admonition type="error">{state.error}</Admonition>}
|
||||||
|
|
||||||
|
<View style={[a.gap_sm, gtPhone && [a.flex_row_reverse]]}>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Send verification email`)}
|
||||||
|
size="large"
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
onPress={handleManageEmail2FA}
|
||||||
|
disabled={state.status === 'pending'}>
|
||||||
|
<ButtonText>Enable</ButtonText>
|
||||||
|
<ButtonIcon
|
||||||
|
position="right"
|
||||||
|
icon={
|
||||||
|
state.status === 'pending'
|
||||||
|
? Loader
|
||||||
|
: state.status === 'success'
|
||||||
|
? Check
|
||||||
|
: ShieldIcon
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Send verification email`)}
|
||||||
|
size="large"
|
||||||
|
variant="solid"
|
||||||
|
color="secondary"
|
||||||
|
onPress={() => control.close()}>
|
||||||
|
<ButtonText>Cancel</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import {useState} from 'react'
|
||||||
|
import {Trans} from '@lingui/macro'
|
||||||
|
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
|
import {useIsEmailVerified} from '#/components/dialogs/EmailDialog/data/useIsEmailVerified'
|
||||||
|
import {Disable} from '#/components/dialogs/EmailDialog/screens/Manage2FA/Disable'
|
||||||
|
/**
|
||||||
|
* Sub-screens
|
||||||
|
*/
|
||||||
|
import {Enable} from '#/components/dialogs/EmailDialog/screens/Manage2FA/Enable'
|
||||||
|
import {type Screen, ScreenID} from '#/components/dialogs/EmailDialog/types'
|
||||||
|
|
||||||
|
export function Manage2FA({
|
||||||
|
showScreen,
|
||||||
|
}: {
|
||||||
|
config: Extract<Screen, {id: 'Manage2FA'}>
|
||||||
|
showScreen: (screen: Screen) => void
|
||||||
|
}) {
|
||||||
|
const [requestedAction, setRequestedAction] = useState<
|
||||||
|
'enable' | 'disable' | null
|
||||||
|
>(null)
|
||||||
|
// TODO confirm this is in sync
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
const {isEmailVerified} = useIsEmailVerified()
|
||||||
|
|
||||||
|
if (!isEmailVerified) {
|
||||||
|
showScreen({
|
||||||
|
id: ScreenID.Verify,
|
||||||
|
instructions: [
|
||||||
|
<Trans key="2fa">
|
||||||
|
You need to verify your email address before you can enable email 2FA.
|
||||||
|
</Trans>,
|
||||||
|
],
|
||||||
|
onVerify: () => {
|
||||||
|
showScreen({
|
||||||
|
id: ScreenID.Manage2FA,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentAccount?.emailAuthFactor && requestedAction !== 'enable') {
|
||||||
|
if (!requestedAction) {
|
||||||
|
setRequestedAction('disable')
|
||||||
|
}
|
||||||
|
return <Disable />
|
||||||
|
} else if (
|
||||||
|
!currentAccount?.emailAuthFactor &&
|
||||||
|
requestedAction !== 'disable'
|
||||||
|
) {
|
||||||
|
if (!requestedAction) {
|
||||||
|
setRequestedAction('enable')
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Enable />
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
|||||||
import {ResendEmailText} from '#/components/dialogs/EmailDialog/components/ResendEmailText'
|
import {ResendEmailText} from '#/components/dialogs/EmailDialog/components/ResendEmailText'
|
||||||
import {TokenField} from '#/components/dialogs/EmailDialog/components/TokenField'
|
import {TokenField} from '#/components/dialogs/EmailDialog/components/TokenField'
|
||||||
import {useConfirmEmail} from '#/components/dialogs/EmailDialog/data/useConfirmEmail'
|
import {useConfirmEmail} from '#/components/dialogs/EmailDialog/data/useConfirmEmail'
|
||||||
|
import {useIsEmailVerified} from '#/components/dialogs/EmailDialog/data/useIsEmailVerified'
|
||||||
import {useRequestEmailVerification} from '#/components/dialogs/EmailDialog/data/useRequestEmailVerification'
|
import {useRequestEmailVerification} from '#/components/dialogs/EmailDialog/data/useRequestEmailVerification'
|
||||||
import {type Screen} from '#/components/dialogs/EmailDialog/types'
|
import {type Screen} from '#/components/dialogs/EmailDialog/types'
|
||||||
import {Divider} from '#/components/Divider'
|
import {Divider} from '#/components/Divider'
|
||||||
@@ -21,7 +22,7 @@ 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'
|
||||||
|
|
||||||
export function Verify({config}: {config: Exclude<Screen, {id: 'Update'}>}) {
|
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()
|
||||||
@@ -39,6 +40,12 @@ export function Verify({config}: {config: Exclude<Screen, {id: 'Update'}>}) {
|
|||||||
)
|
)
|
||||||
const {mutateAsync: confirmEmail} = useConfirmEmail()
|
const {mutateAsync: confirmEmail} = useConfirmEmail()
|
||||||
|
|
||||||
|
useIsEmailVerified({
|
||||||
|
onEmailVerified: () => {
|
||||||
|
config.onVerify?.()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const handleRequestEmailVerification = async () => {
|
const handleRequestEmailVerification = async () => {
|
||||||
setError('')
|
setError('')
|
||||||
setSendingStatus('sending')
|
setSendingStatus('sending')
|
||||||
|
|||||||
@@ -16,17 +16,14 @@ export type Screen =
|
|||||||
id: ScreenID.Verify
|
id: ScreenID.Verify
|
||||||
instructions?: ReactNode[]
|
instructions?: ReactNode[]
|
||||||
hideInitialCodeButton?: boolean
|
hideInitialCodeButton?: boolean
|
||||||
|
onVerify?: () => void
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
id: ScreenID.Enable2FA
|
id: ScreenID.Manage2FA
|
||||||
}
|
|
||||||
| {
|
|
||||||
id: ScreenID.Disable2FA
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ScreenID {
|
export enum ScreenID {
|
||||||
Update = 'Update',
|
Update = 'Update',
|
||||||
Verify = 'Verify',
|
Verify = 'Verify',
|
||||||
Enable2FA = 'Enable2FA',
|
Manage2FA = 'Manage2FA',
|
||||||
Disable2FA = 'Disable2FA',
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import {
|
import {
|
||||||
EmailDialog,
|
EmailDialog,
|
||||||
EmailDialogScreenID,
|
EmailDialogScreenID,
|
||||||
useEmailDialogControl,
|
useEmailDialogControl,
|
||||||
} from '#/components/dialogs/EmailDialog'
|
} from '#/components/dialogs/EmailDialog'
|
||||||
import * as Prompt from '#/components/Prompt'
|
|
||||||
import {DisableEmail2FADialog} from './DisableEmail2FADialog'
|
import {DisableEmail2FADialog} from './DisableEmail2FADialog'
|
||||||
import * as SettingsList from './SettingsList'
|
import * as SettingsList from './SettingsList'
|
||||||
|
|
||||||
@@ -17,59 +16,17 @@ export function Email2FAToggle() {
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const disableDialogControl = useDialogControl()
|
const disableDialogControl = useDialogControl()
|
||||||
const enableDialogControl = useDialogControl()
|
|
||||||
const agent = useAgent()
|
|
||||||
const emailDialogControl = useEmailDialogControl()
|
const emailDialogControl = useEmailDialogControl()
|
||||||
|
|
||||||
const enableEmailAuthFactor = React.useCallback(async () => {
|
|
||||||
if (currentAccount?.email) {
|
|
||||||
await agent.com.atproto.server.updateEmail({
|
|
||||||
email: currentAccount.email,
|
|
||||||
emailAuthFactor: true,
|
|
||||||
})
|
|
||||||
await agent.resumeSession(agent.session!)
|
|
||||||
}
|
|
||||||
}, [currentAccount, agent])
|
|
||||||
|
|
||||||
const onToggle = React.useCallback(() => {
|
const onToggle = React.useCallback(() => {
|
||||||
if (!currentAccount) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (currentAccount.emailAuthFactor) {
|
|
||||||
disableDialogControl.open()
|
|
||||||
} else {
|
|
||||||
if (!currentAccount.emailConfirmed) {
|
|
||||||
emailDialogControl.open({
|
emailDialogControl.open({
|
||||||
id: EmailDialogScreenID.Verify,
|
id: EmailDialogScreenID.Manage2FA,
|
||||||
hideInitialCodeButton: true,
|
|
||||||
instructions: [
|
|
||||||
<Trans key="2fa">
|
|
||||||
You need to verify your email address before you can enable email
|
|
||||||
2FA.
|
|
||||||
</Trans>,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
return
|
}, [emailDialogControl])
|
||||||
}
|
|
||||||
enableDialogControl.open()
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
currentAccount,
|
|
||||||
enableDialogControl,
|
|
||||||
disableDialogControl,
|
|
||||||
emailDialogControl,
|
|
||||||
])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DisableEmail2FADialog control={disableDialogControl} />
|
<DisableEmail2FADialog control={disableDialogControl} />
|
||||||
<Prompt.Basic
|
|
||||||
control={enableDialogControl}
|
|
||||||
title={_(msg`Enable Email 2FA`)}
|
|
||||||
description={_(msg`Require an email code to sign in to your account.`)}
|
|
||||||
onConfirm={enableEmailAuthFactor}
|
|
||||||
confirmButtonCta={_(msg`Enable`)}
|
|
||||||
/>
|
|
||||||
<EmailDialog control={emailDialogControl} />
|
<EmailDialog control={emailDialogControl} />
|
||||||
<SettingsList.BadgeButton
|
<SettingsList.BadgeButton
|
||||||
label={
|
label={
|
||||||
|
|||||||
Reference in New Issue
Block a user