Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Email auth factor #3602

Merged
merged 7 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
},
"dependencies": {
"@atproto/api": "^0.12.3",
"@atproto/api": "^0.12.5",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "https://github.com/bluesky-social/react-native-bottom-sheet.git#discord-fork-4.6.1",
Expand Down
46 changes: 44 additions & 2 deletions src/screens/Login/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import {
TextInput,
View,
} from 'react-native'
import {ComAtprotoServerDescribeServer} from '@atproto/api'
import {
ComAtprotoServerCreateSession,
ComAtprotoServerDescribeServer,
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'

Expand All @@ -23,6 +26,7 @@ import {HostingProvider} from '#/components/forms/HostingProvider'
import * as TextField from '#/components/forms/TextField'
import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {FormContainer} from './FormContainer'
Expand Down Expand Up @@ -53,8 +57,11 @@ export const LoginForm = ({
const {track} = useAnalytics()
const t = useTheme()
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] =
useState<boolean>(false)
const [identifier, setIdentifier] = useState<string>(initialHandle)
const [password, setPassword] = useState<string>('')
const [authFactorToken, setAuthFactorToken] = useState<string>('')
const passwordInputRef = useRef<TextInput>(null)
const {_} = useLingui()
const {login} = useSessionApi()
Expand Down Expand Up @@ -100,14 +107,19 @@ export const LoginForm = ({
service: serviceUrl,
identifier: fullIdent,
password,
authFactorToken,
pfrazee marked this conversation as resolved.
Show resolved Hide resolved
},
'LoginForm',
)
} catch (e: any) {
const errMsg = e.toString()
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
setIsProcessing(false)
if (errMsg.includes('Authentication Required')) {
if (
e instanceof ComAtprotoServerCreateSession.AuthFactorTokenRequiredError
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note instanceof doesn't work across iframes so it would be nice not to rely on this as the recommended way to check it

) {
setIsAuthFactorTokenNeeded(true)
} else if (errMsg.includes('Authentication Required')) {
logger.debug('Failed to login due to invalid credentials', {
error: errMsg,
})
Expand Down Expand Up @@ -215,6 +227,36 @@ export const LoginForm = ({
</TextField.Root>
</View>
</View>
{isAuthFactorTokenNeeded && (
<View>
<TextField.LabelText>
<Trans>2FA Confirmation</Trans>
</TextField.LabelText>
<TextField.Root>
<TextField.Icon icon={Ticket} />
<TextField.Input
pfrazee marked this conversation as resolved.
Show resolved Hide resolved
testID="loginAuthFactorTokenInput"
label={_(msg`Confirmation code`)}
autoCapitalize="none"
autoFocus
autoCorrect={false}
autoComplete="off"
returnKeyType="done"
textContentType="username"
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
value={authFactorToken}
onChangeText={setAuthFactorToken}
editable={!isProcessing}
accessibilityHint={_(
msg`Input the code which has been emailed to you`,
)}
/>
</TextField.Root>
<Text style={[a.text_sm, t.atoms.text_contrast_medium, a.mt_sm]}>
<Trans>Check your email for a login code and enter it here.</Trans>
</Text>
</View>
)}
<FormError error={error} />
<View style={[a.flex_row, a.align_center, a.pt_md]}>
<Button
Expand Down
1 change: 1 addition & 0 deletions src/state/modals/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export interface PostLanguagesSettingsModal {
export interface VerifyEmailModal {
name: 'verify-email'
showReminder?: boolean
onSuccess?: () => void
}

export interface ChangeEmailModal {
Expand Down
1 change: 1 addition & 0 deletions src/state/persisted/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const accountSchema = z.object({
handle: z.string(),
email: z.string().optional(),
emailConfirmed: z.boolean().optional(),
emailAuthFactor: z.boolean().optional(),
refreshJwt: z.string().optional(), // optional because it can expire
accessJwt: z.string().optional(), // optional because it can expire
deactivated: z.boolean().optional(),
Expand Down
16 changes: 13 additions & 3 deletions src/state/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export type ApiContext = {
service: string
identifier: string
password: string
authFactorToken?: string | undefined
},
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
Expand Down Expand Up @@ -87,7 +88,10 @@ export type ApiContext = {
) => Promise<void>
updateCurrentAccount: (
account: Partial<
Pick<SessionAccount, 'handle' | 'email' | 'emailConfirmed'>
Pick<
SessionAccount,
'handle' | 'email' | 'emailConfirmed' | 'emailAuthFactor'
>
>,
) => void
}
Expand Down Expand Up @@ -293,12 +297,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
)

const login = React.useCallback<ApiContext['login']>(
async ({service, identifier, password}, logContext) => {
async ({service, identifier, password, authFactorToken}, logContext) => {
logger.debug(`session: login`, {}, logger.DebugContext.session)

const agent = new BskyAgent({service})

await agent.login({identifier, password})
await agent.login({identifier, password, authFactorToken})

if (!agent.session) {
throw new Error(`session: login failed to establish a session`)
Expand All @@ -310,6 +314,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
handle: agent.session.handle,
email: agent.session.email,
emailConfirmed: agent.session.emailConfirmed || false,
emailAuthFactor: agent.session.emailAuthFactor,
refreshJwt: agent.session.refreshJwt,
accessJwt: agent.session.accessJwt,
deactivated: isSessionDeactivated(agent.session.accessJwt),
Expand Down Expand Up @@ -476,6 +481,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
handle: agent.session.handle,
email: agent.session.email,
emailConfirmed: agent.session.emailConfirmed || false,
emailAuthFactor: agent.session.emailAuthFactor || false,
refreshJwt: agent.session.refreshJwt,
accessJwt: agent.session.accessJwt,
deactivated: isSessionDeactivated(agent.session.accessJwt),
Expand Down Expand Up @@ -533,6 +539,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
account.emailConfirmed !== undefined
? account.emailConfirmed
: currentAccount.emailConfirmed,
emailAuthFactor:
account.emailAuthFactor !== undefined
? account.emailAuthFactor
: currentAccount.emailAuthFactor,
}

return {
Expand Down
36 changes: 22 additions & 14 deletions src/view/com/modals/VerifyEmail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,24 @@ import {
StyleSheet,
View,
} from 'react-native'
import {Svg, Circle, Path} from 'react-native-svg'
import {ScrollView, TextInput} from './util'
import {Circle, Path, Svg} from 'react-native-svg'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from '../util/text/Text'
import {Button} from '../util/forms/Button'
import {ErrorMessage} from '../util/error/ErrorMessage'
import * as Toast from '../util/Toast'
import {s, colors} from 'lib/styles'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'

import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {getAgent, useSession, useSessionApi} from '#/state/session'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError} from 'lib/strings/errors'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useSession, useSessionApi, getAgent} from '#/state/session'
import {logger} from '#/logger'
import {colors, s} from 'lib/styles'
import {isWeb} from 'platform/detection'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Button} from '../util/forms/Button'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
import {ScrollView, TextInput} from './util'

export const snapPoints = ['90%']

Expand All @@ -32,7 +33,13 @@ enum Stages {
ConfirmCode,
}

export function Component({showReminder}: {showReminder?: boolean}) {
export function Component({
showReminder,
onSuccess,
}: {
showReminder?: boolean
onSuccess?: () => void
}) {
const pal = usePalette('default')
const {currentAccount} = useSession()
const {updateCurrentAccount} = useSessionApi()
Expand Down Expand Up @@ -77,6 +84,7 @@ export function Component({showReminder}: {showReminder?: boolean}) {
updateCurrentAccount({emailConfirmed: true})
Toast.show(_(msg`Email verified`))
closeModal()
onSuccess?.()
} catch (e) {
setError(cleanError(String(e)))
} finally {
Expand Down
Loading
Loading