move auth screens into the navigation system

This commit is contained in:
Samuel Newman
2025-07-02 14:17:24 +03:00
parent 4ccbae7c30
commit 5f0b20ab6c
16 changed files with 897 additions and 9 deletions
+79
View File
@@ -15,6 +15,7 @@ import {
NavigationContainer,
StackActions,
} from '@react-navigation/native'
import {createNativeStackNavigator} from '@react-navigation/native-stack'
import {timeout} from '#/lib/async/timeout'
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
@@ -30,6 +31,7 @@ import {buildStateObject} from '#/lib/routes/helpers'
import {
type AllNavigatorParams,
type BottomTabNavigatorParams,
type CoreNavigatorParams,
type FlatNavigatorParams,
type HomeTabNavigatorParams,
type MessagesTabNavigatorParams,
@@ -71,6 +73,16 @@ import {SupportScreen} from '#/view/screens/Support'
import {TermsOfServiceScreen} from '#/view/screens/TermsOfService'
import {BottomBar} from '#/view/shell/bottom-bar/BottomBar'
import {createNativeStackNavigatorWithAuth} from '#/view/shell/createNativeStackNavigatorWithAuth'
import {ForgotPasswordScreen} from '#/screens/Authentication/ForgotPasswordScreen'
import {LandingScreen} from '#/screens/Authentication/LandingScreen'
import {PasswordUpdatedScreen} from '#/screens/Authentication/PasswordUpdatedScreen'
import {SelectAccountScreen} from '#/screens/Authentication/SelectAccountScreen'
import {SetNewPasswordScreen} from '#/screens/Authentication/SetNewPasswordScreen'
import {SignInScreen} from '#/screens/Authentication/SignInScreen'
import {SignUpCaptchaScreen} from '#/screens/Authentication/SignUpCaptchaScreen'
import {SignUpHandleScreen} from '#/screens/Authentication/SignUpHandleScreen'
import {SignUpInfoScreen} from '#/screens/Authentication/SignUpInfoScreen'
import {StarterPackLandingScreen} from '#/screens/Authentication/StarterPackLandingScreen'
import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTesterScreen'
import HashtagScreen from '#/screens/Hashtag'
import {MessagesScreen} from '#/screens/Messages/ChatList'
@@ -135,10 +147,12 @@ import {Referrer} from '../modules/expo-bluesky-swiss-army'
import {useAccountSwitcher} from './lib/hooks/useAccountSwitcher'
import {useNonReactiveCallback} from './lib/hooks/useNonReactiveCallback'
import {useLoggedOutViewControls} from './state/shell/logged-out'
import {useLoggedOutView} from './state/shell/logged-out'
import {useCloseAllActiveElements} from './state/util'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
const Core = createNativeStackNavigator<CoreNavigatorParams>()
const HomeTab = createNativeStackNavigatorWithAuth<HomeTabNavigatorParams>()
const SearchTab = createNativeStackNavigatorWithAuth<SearchTabNavigatorParams>()
const NotificationsTab =
@@ -768,6 +782,71 @@ const FlatNavigator = () => {
)
}
/**
* The core navigator on native handles authentication, switching between
* the main tab navigator when logged in and the auth screen when logged out.
*/
export const NativeNavigator = () => {
const t = useTheme()
const {hasSession} = useSession()
const {showLoggedOut} = useLoggedOutView()
return (
<Core.Navigator screenOptions={screenOptions(t)}>
{hasSession && !showLoggedOut ? (
<Core.Screen
name="App"
getComponent={() => TabsNavigator}
options={{animation: 'fade', animationDuration: 200}}
/>
) : (
<>
<Core.Screen
name="Landing"
getComponent={() => LandingScreen}
options={{animation: 'fade', animationDuration: 200}}
/>
<Core.Screen
name="StarterPackLanding"
getComponent={() => StarterPackLandingScreen}
/>
<Core.Group>
<Core.Screen
name="SelectAccount"
getComponent={() => SelectAccountScreen}
/>
<Core.Screen name="SignIn" getComponent={() => SignInScreen} />
<Core.Screen
name="ForgotPassword"
getComponent={() => ForgotPasswordScreen}
/>
<Core.Screen
name="SetNewPassword"
getComponent={() => SetNewPasswordScreen}
/>
<Core.Screen
name="PasswordUpdated"
getComponent={() => PasswordUpdatedScreen}
/>
<Core.Screen
name="SignUpInfo"
getComponent={() => SignUpInfoScreen}
/>
<Core.Screen
name="SignUpHandle"
getComponent={() => SignUpHandleScreen}
/>
<Core.Screen
name="SignUpCaptcha"
getComponent={() => SignUpCaptchaScreen}
/>
</Core.Group>
</>
)}
</Core.Navigator>
)
}
/**
* The RoutesContainer should wrap all components which need access
* to the navigation context.
+28
View File
@@ -1,6 +1,7 @@
import {type NavigationState, type PartialState} from '@react-navigation/native'
import {type NativeStackNavigationProp} from '@react-navigation/native-stack'
import {type SessionAccount} from '#/state/session'
import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types'
export type {NativeStackScreenProps} from '@react-navigation/native-stack'
@@ -133,6 +134,33 @@ export type AllNavigatorParams = CommonNavigatorParams & {
Messages: {animation?: 'push' | 'pop'}
}
/**
* Authentication screens are separate from the main navigator -sfn
*/
export type AuthNavigatorParams = {
Landing: undefined
StarterPackLanding: {uri: string}
SelectAccount: undefined
// TODO: Confirm this is not via query params on web
// Move to context instead if so
SignIn?: {account?: SessionAccount}
ForgotPassword: undefined
SetNewPassword: undefined
PasswordUpdated: undefined
SignUpInfo: undefined
SignUpHandle: undefined
SignUpCaptcha: undefined
}
/**
* On native, the root navigator is a stack navigator that switches between
* the tab navigator, in the `App` screen, and the auth screens.
* which screens are mounted depends on the auth state -sfn
*/
export type CoreNavigatorParams = {
App: undefined
} & AuthNavigatorParams
// NOTE
// this isn't strictly correct but it should be close enough
// a TS wizard might be able to get this 100%
@@ -0,0 +1,22 @@
import {
type AuthNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'ForgotPassword'>
export function ForgotPasswordScreen({}: Props) {
return (
<Layout.Screen testID="ForgotPasswordScreen">
<Layout.Header.Outer noBottomBorder>
<Layout.Header.BackButton />
<Layout.Header.Content />
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<Text>TODO</Text>
</Layout.Content>
</Layout.Screen>
)
}
@@ -0,0 +1,109 @@
import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
type AuthNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {useSession} from '#/state/session'
import {
useLoggedOutView,
useLoggedOutViewControls,
} from '#/state/shell/logged-out'
import {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype'
import {atoms as a, useTheme} from '#/alf'
import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as CloseIcon} from '#/components/icons/Times'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'Landing'>
export function LandingScreen({navigation}: Props) {
const {_} = useLingui()
const t = useTheme()
const insets = useSafeAreaInsets()
const {accounts} = useSession()
const {showLoggedOut, requestedAccountSwitchTo} = useLoggedOutView()
const {setShowLoggedOut} = useLoggedOutViewControls()
const requestedAccount = accounts.find(
acc => acc.did === requestedAccountSwitchTo,
)
return (
<Layout.Screen
testID="LandingScreen"
style={{paddingBottom: insets.bottom}}>
{showLoggedOut && (
<Layout.Header.Outer noBottomBorder>
<Layout.Header.Slot />
<Layout.Header.Content />
<Layout.Header.Slot>
<Button
label={_(msg`Close`)}
onPress={() => setShowLoggedOut(false)}
size="small"
color="secondary_inverted"
shape="round"
variant="solid">
<ButtonIcon icon={CloseIcon} />
</Button>
</Layout.Header.Slot>
</Layout.Header.Outer>
)}
<View style={[a.flex_1, a.justify_center, a.align_center]}>
<Logo width={92} fill="sky" />
<View style={[a.pb_sm, a.pt_5xl]}>
<Logotype width={161} fill={t.atoms.text.color} />
</View>
<Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}>
<Trans>What's up?</Trans>
</Text>
</View>
<View
testID="signinOrCreateAccount"
style={[a.px_xl, a.gap_md, a.pb_2xl]}>
<Button
testID="createAccountButton"
onPress={() => navigation.push('SignUpInfo')}
label={_(msg`Create new account`)}
size="large"
variant="solid"
color="primary">
<ButtonText>
<Trans>Create account</Trans>
</ButtonText>
</Button>
<Button
testID="signInButton"
onPress={() => {
if (requestedAccount) {
navigation.push('SignIn', {account: requestedAccount})
} else {
navigation.push(accounts.length > 0 ? 'SelectAccount' : 'SignIn')
}
}}
label={_(msg`Sign in`)}
size="large"
variant="solid"
color="secondary">
<ButtonText>
<Trans>Sign in</Trans>
</ButtonText>
</Button>
</View>
<View
style={[a.px_lg, a.pt_md, a.pb_2xl, a.justify_center, a.align_center]}>
<View>
<AppLanguageDropdown />
</View>
</View>
</Layout.Screen>
)
}
@@ -0,0 +1,22 @@
import {
type AuthNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'PasswordUpdated'>
export function PasswordUpdatedScreen({}: Props) {
return (
<Layout.Screen testID="PasswordUpdatedScreen">
<Layout.Header.Outer noBottomBorder>
<Layout.Header.BackButton />
<Layout.Header.Content />
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<Text>TODO</Text>
</Layout.Content>
</Layout.Screen>
)
}
@@ -0,0 +1,78 @@
import {useState} from 'react'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
type AuthNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {logger} from '#/logger'
import {type SessionAccount, useSession, useSessionApi} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, native} from '#/alf'
import {AccountList} from '#/components/AccountList'
import * as TextField from '#/components/forms/TextField'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'SelectAccount'>
export function SelectAccountScreen({navigation}: Props) {
const [pendingDid, setPendingDid] = useState<string | null>(null)
const {_} = useLingui()
const {currentAccount} = useSession()
const {resumeSession} = useSessionApi()
const {setShowLoggedOut} = useLoggedOutViewControls()
const onSelect = async (account: SessionAccount) => {
if (pendingDid) {
// The session API isn't resilient to race conditions so let's just ignore this.
return
}
if (!account.accessJwt) {
// Move to login form.
navigation.push('SignIn', {account})
return
}
if (account.did === currentAccount?.did) {
setShowLoggedOut(false)
return
}
try {
setPendingDid(account.did)
await resumeSession(account)
logger.metric('account:loggedIn', {
logContext: 'ChooseAccountForm',
withPassword: false,
})
Toast.show(_(msg`Signed in as @${account.handle}`))
} catch (e: any) {
logger.error('choose account: initSession failed', {
message: e.message,
})
// Move to login form.
navigation.push('SignIn', {account})
} finally {
setPendingDid(null)
}
}
return (
<Layout.Screen testID="SelectAccountScreen">
<Layout.Header.Outer noBottomBorder>
<Layout.Header.BackButton />
<Layout.Header.Content />
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content contentContainerStyle={[a.py_xl, native(a.px_xl)]}>
<TextField.LabelText>
<Trans>Sign in as...</Trans>
</TextField.LabelText>
<AccountList
onSelectAccount={onSelect}
onSelectOther={() => navigation.push('SignIn')}
pendingDid={pendingDid}
/>
</Layout.Content>
</Layout.Screen>
)
}
@@ -0,0 +1,22 @@
import {
type AuthNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'SetNewPassword'>
export function SetNewPasswordScreen({}: Props) {
return (
<Layout.Screen testID="SetNewPasswordScreen">
<Layout.Header.Outer noBottomBorder>
<Layout.Header.BackButton />
<Layout.Header.Content />
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<Text>TODO</Text>
</Layout.Content>
</Layout.Screen>
)
}
+105
View File
@@ -0,0 +1,105 @@
import {useEffect, useRef, useState} from 'react'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {DEFAULT_SERVICE} from '#/lib/constants'
import {
type AuthNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {logger} from '#/logger'
import {useServiceQuery} from '#/state/queries/service'
import {atoms as a, useTheme} from '#/alf'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
import {SignInForm} from './components/SignInForm'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'SignIn'>
export function SignInScreen({
navigation,
route: {params: {account: requestedAccount} = {}},
}: Props) {
const t = useTheme()
const {_} = useLingui()
const failedAttemptCountRef = useRef(0)
const startTimeRef = useRef(Date.now())
const [error, setError] = useState<string>('')
const [serviceUrl, setServiceUrl] = useState<string>(
requestedAccount?.service || DEFAULT_SERVICE,
)
const {
data: serviceDescription,
error: serviceError,
refetch: refetchService,
} = useServiceQuery(serviceUrl)
useEffect(() => {
if (serviceError) {
setError(
_(
msg`Unable to contact your service. Please check your Internet connection.`,
),
)
logger.warn(`Failed to fetch service description for ${serviceUrl}`, {
error: String(serviceError),
})
logger.metric('signin:hostingProviderFailedResolution', {})
} else {
setError('')
}
}, [serviceError, serviceUrl, _])
const onPressForgotPassword = () => {
navigation.push('ForgotPassword')
logger.metric('signin:forgotPasswordPressed', {})
}
const onAttemptSuccess = () => {
logger.metric('signin:success', {
isUsingCustomProvider: serviceUrl !== DEFAULT_SERVICE,
timeTakenSeconds: Math.round((Date.now() - startTimeRef.current) / 1000),
failedAttemptsCount: failedAttemptCountRef.current,
})
}
const onAttemptFailed = () => {
failedAttemptCountRef.current += 1
}
return (
<Layout.Screen testID="SignInScreen">
<Layout.Header.Outer noBottomBorder>
<Layout.Header.BackButton />
<Layout.Header.Content />
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content contentContainerStyle={[a.px_xl]}>
<Text style={[a.font_heavy, a.text_3xl]}>Log in</Text>
<SignInForm
error={error}
serviceUrl={serviceUrl}
serviceDescription={serviceDescription}
initialHandle={requestedAccount?.handle}
setError={setError}
onAttemptFailed={onAttemptFailed}
onAttemptSuccess={onAttemptSuccess}
setServiceUrl={setServiceUrl}
onPressForgotPassword={onPressForgotPassword}
onPressRetryConnect={refetchService}
/>
<Text style={[a.text_md, a.text_center, a.w_full, a.mt_2xl]}>
<Trans>
New to Bluesky?{' '}
<Text
role="link"
onPress={() => navigation.push('SignUpInfo')}
style={[a.text_md, {color: t.palette.primary_500}]}>
Create account
</Text>
</Trans>
</Text>
</Layout.Content>
</Layout.Screen>
)
}
@@ -0,0 +1,22 @@
import {
type AuthNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'SignUpCaptcha'>
export function SignUpCaptchaScreen({}: Props) {
return (
<Layout.Screen testID="SignUpCaptchaScreen">
<Layout.Header.Outer noBottomBorder>
<Layout.Header.BackButton />
<Layout.Header.Content />
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<Text>TODO</Text>
</Layout.Content>
</Layout.Screen>
)
}
@@ -0,0 +1,22 @@
import {
type AuthNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'SignUpHandle'>
export function SignUpHandleScreen({}: Props) {
return (
<Layout.Screen testID="SignUpHandleScreen">
<Layout.Header.Outer noBottomBorder>
<Layout.Header.BackButton />
<Layout.Header.Content />
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<Text>TODO</Text>
</Layout.Content>
</Layout.Screen>
)
}
@@ -0,0 +1,22 @@
import {
type AuthNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'SignUpInfo'>
export function SignUpInfoScreen({}: Props) {
return (
<Layout.Screen testID="SignUpInfoScreen">
<Layout.Header.Outer noBottomBorder>
<Layout.Header.BackButton />
<Layout.Header.Content />
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<Text>TODO</Text>
</Layout.Content>
</Layout.Screen>
)
}
@@ -0,0 +1,17 @@
import {
type AuthNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'StarterPackLanding'>
export function StarterPackLandingScreen({}: Props) {
return (
<Layout.Screen testID="StarterPackLandingScreen">
<Layout.Content>
<Text>TODO</Text>
</Layout.Content>
</Layout.Screen>
)
}
@@ -0,0 +1,340 @@
import {useRef, useState} from 'react'
import {
ActivityIndicator,
Keyboard,
LayoutAnimation,
type TextInput,
View,
} from 'react-native'
import {
ComAtprotoServerCreateSession,
type ComAtprotoServerDescribeServer,
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
import {isNetworkError} from '#/lib/strings/errors'
import {cleanError} from '#/lib/strings/errors'
import {createFullHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
import {useSessionApi} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {FormError} from '#/components/forms/FormError'
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'
type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
export function SignInForm({
error,
serviceUrl,
serviceDescription,
initialHandle = '',
setError,
// setServiceUrl,
onPressRetryConnect,
onPressForgotPassword,
onAttemptSuccess,
onAttemptFailed,
}: {
error: string
serviceUrl: string
serviceDescription: ServiceDescription | undefined
initialHandle?: string
setError: (v: string) => void
setServiceUrl: (v: string) => void
onPressRetryConnect: () => void
onPressForgotPassword: () => void
onAttemptSuccess: () => void
onAttemptFailed: () => void
}) {
const t = useTheme()
const [isProcessing, setIsProcessing] = useState(false)
const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] = useState(false)
const [isAuthFactorTokenValueEmpty, setIsAuthFactorTokenValueEmpty] =
useState(true)
const identifierValueRef = useRef(initialHandle || '')
const passwordValueRef = useRef('')
const authFactorTokenValueRef = useRef('')
const passwordRef = useRef<TextInput>(null)
const {_} = useLingui()
const {login} = useSessionApi()
const requestNotificationsPermission = useRequestNotificationsPermission()
const {setShowLoggedOut} = useLoggedOutViewControls()
const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack()
// const onPressSelectService = () => {
// Keyboard.dismiss()
// }
const onPressNext = async () => {
if (isProcessing) return
Keyboard.dismiss()
setError('')
const identifier = identifierValueRef.current.toLowerCase().trim()
const password = passwordValueRef.current
const authFactorToken = authFactorTokenValueRef.current
if (!identifier) {
setError(_(msg`Please enter your username`))
return
}
if (!password) {
setError(_(msg`Please enter your password`))
return
}
setIsProcessing(true)
try {
// try to guess the handle if the user just gave their own username
let fullIdent = identifier
if (
!identifier.includes('@') && // not an email
!identifier.includes('.') && // not a domain
serviceDescription &&
serviceDescription.availableUserDomains.length > 0
) {
let matched = false
for (const domain of serviceDescription.availableUserDomains) {
if (fullIdent.endsWith(domain)) {
matched = true
}
}
if (!matched) {
fullIdent = createFullHandle(
identifier,
serviceDescription.availableUserDomains[0],
)
}
}
// TODO remove double login
await login(
{
service: serviceUrl,
identifier: fullIdent,
password,
authFactorToken: authFactorToken.trim(),
},
'LoginForm',
)
onAttemptSuccess()
setShowLoggedOut(false)
setHasCheckedForStarterPack(true)
requestNotificationsPermission('Login')
} catch (e: any) {
const errMsg = e.toString()
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
setIsProcessing(false)
if (
e instanceof ComAtprotoServerCreateSession.AuthFactorTokenRequiredError
) {
setIsAuthFactorTokenNeeded(true)
} else {
onAttemptFailed()
if (errMsg.includes('Token is invalid')) {
logger.debug('Failed to login due to invalid 2fa token', {
error: errMsg,
})
setError(_(msg`Invalid 2FA confirmation code.`))
} else if (
errMsg.includes('Authentication Required') ||
errMsg.includes('Invalid identifier or password')
) {
logger.debug('Failed to login due to invalid credentials', {
error: errMsg,
})
setError(_(msg`Incorrect username or password`))
} else if (isNetworkError(e)) {
logger.warn('Failed to login due to network error', {error: errMsg})
setError(
_(
msg`Unable to contact your service. Please check your Internet connection.`,
),
)
} else {
logger.warn('Failed to login', {error: errMsg})
setError(cleanError(errMsg))
}
}
}
}
return (
<View style={[a.pt_5xl, a.gap_md]}>
<View>
<TextField.LabelText>
<Trans>Account</Trans>
</TextField.LabelText>
<View style={[a.gap_sm]}>
<TextField.Root>
<TextField.Icon icon={At} />
<TextField.Input
testID="loginUsernameInput"
label={_(msg`Username or email address`)}
autoCapitalize="none"
autoFocus
autoCorrect={false}
autoComplete="username"
returnKeyType="next"
textContentType="username"
defaultValue={initialHandle || ''}
onChangeText={v => {
identifierValueRef.current = v
}}
onSubmitEditing={() => {
passwordRef.current?.focus()
}}
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
editable={!isProcessing}
accessibilityHint={_(
msg`Enter the username or email address you used when you created your account`,
)}
/>
</TextField.Root>
<TextField.Root>
<TextField.Icon icon={Lock} />
<TextField.Input
testID="loginPasswordInput"
inputRef={passwordRef}
label={_(msg`Password`)}
autoCapitalize="none"
autoCorrect={false}
autoComplete="password"
returnKeyType="done"
enablesReturnKeyAutomatically={true}
secureTextEntry={true}
textContentType="password"
clearButtonMode="while-editing"
onChangeText={v => {
passwordValueRef.current = v
}}
onSubmitEditing={onPressNext}
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
editable={!isProcessing}
accessibilityHint={_(msg`Enter your password`)}
/>
<Button
testID="forgotPasswordButton"
onPress={onPressForgotPassword}
label={_(msg`Forgot password?`)}
accessibilityHint={_(msg`Opens password reset form`)}
variant="solid"
color="secondary"
style={[
a.rounded_sm,
// t.atoms.bg_contrast_100,
{marginLeft: 'auto', left: 6, padding: 6},
a.z_10,
]}>
<ButtonText>
<Trans>Forgot?</Trans>
</ButtonText>
</Button>
</TextField.Root>
</View>
</View>
{isAuthFactorTokenNeeded && (
<View>
<TextField.LabelText>
<Trans>2FA Confirmation</Trans>
</TextField.LabelText>
<TextField.Root>
<TextField.Icon icon={Ticket} />
<TextField.Input
testID="loginAuthFactorTokenInput"
label={_(msg`Confirmation code`)}
autoCapitalize="none"
autoFocus
autoCorrect={false}
autoComplete="one-time-code"
returnKeyType="done"
textContentType="username"
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
onChangeText={v => {
setIsAuthFactorTokenValueEmpty(v === '')
authFactorTokenValueRef.current = v
}}
onSubmitEditing={onPressNext}
editable={!isProcessing}
accessibilityHint={_(
msg`Input the code which has been emailed to you`,
)}
style={[
{
textTransform: isAuthFactorTokenValueEmpty
? 'none'
: 'uppercase',
},
]}
/>
</TextField.Root>
<Text style={[a.text_sm, t.atoms.text_contrast_medium, a.mt_sm]}>
<Trans>
Check your email for a sign in code and enter it here.
</Trans>
</Text>
</View>
)}
<FormError error={error} />
<View style={[a.pt_md]}>
{!serviceDescription && error ? (
<Button
testID="loginRetryButton"
label={_(msg`Retry`)}
accessibilityHint={_(msg`Retries signing in`)}
variant="solid"
color="secondary"
size="large"
onPress={onPressRetryConnect}>
<ButtonText>
<Trans>Retry</Trans>
</ButtonText>
</Button>
) : !serviceDescription ? (
<>
<ActivityIndicator />
<Text style={[t.atoms.text_contrast_high, a.pl_md]}>
<Trans>Connecting...</Trans>
</Text>
</>
) : (
<Button
testID="loginNextButton"
label={_(msg`Log in`)}
accessibilityHint={_(
msg`Attempts to log in using the provided credentials`,
)}
variant="solid"
color="primary"
size="large"
onPress={onPressNext}>
<ButtonText>
<Trans>Log in</Trans>
</ButtonText>
{isProcessing && <ButtonIcon icon={Loader} />}
</Button>
)}
</View>
{/* <HostingProvider
serviceUrl={serviceUrl}
onSelectServiceUrl={setServiceUrl}
onOpenDialog={onPressSelectService}
minimal
/> */}
</View>
)
}
+3 -4
View File
@@ -1,5 +1,5 @@
import {ComponentProps} from 'react'
import {StyleSheet, TouchableWithoutFeedback} from 'react-native'
import {type ComponentProps} from 'react'
import {StyleSheet} from 'react-native'
import Animated from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {LinearGradient} from 'expo-linear-gradient'
@@ -13,8 +13,7 @@ import {gradients} from '#/lib/styles'
import {isWeb} from '#/platform/detection'
import {ios} from '#/alf'
export interface FABProps
extends ComponentProps<typeof TouchableWithoutFeedback> {
export interface FABProps extends ComponentProps<typeof PressableScale> {
testID?: string
icon: JSX.Element
}
@@ -27,7 +27,7 @@ import {
import {PWI_ENABLED} from '#/lib/build-flags'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {isNative, isWeb} from '#/platform/detection'
import {isWeb} from '#/platform/detection'
import {useSession} from '#/state/session'
import {useOnboardingState} from '#/state/shell'
import {
@@ -112,7 +112,8 @@ function NativeStackNavigator({
const {setShowLoggedOut} = useLoggedOutViewControls()
const {isMobile} = useWebMediaQueries()
const {leftNavMinimal} = useLayoutBreakpoints()
if (!hasSession && (!PWI_ENABLED || activeRouteRequiresAuth || isNative)) {
// Temp: use old system for web. TODO: unify
if (isWeb && !hasSession && (!PWI_ENABLED || activeRouteRequiresAuth)) {
return <LoggedOut />
}
if (hasSession && currentAccount?.signupQueued) {
@@ -121,7 +122,7 @@ function NativeStackNavigator({
if (hasSession && currentAccount?.status === 'takendown') {
return <Takendown />
}
if (showLoggedOut) {
if (isWeb && showLoggedOut) {
return <LoggedOut onDismiss={() => setShowLoggedOut(false)} />
}
if (currentAccount?.status === 'deactivated') {
+2 -2
View File
@@ -31,7 +31,7 @@ import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
import {SigninDialog} from '#/components/dialogs/Signin'
import {Outlet as PortalOutlet} from '#/components/Portal'
import {RoutesContainer, TabsNavigator} from '#/Navigation'
import {NativeNavigator, RoutesContainer} from '#/Navigation'
import {BottomSheetOutlet} from '../../../modules/bottom-sheet'
import {updateActiveViewAsync} from '../../../modules/expo-bluesky-swiss-army/src/VisibilityView'
import {Composer} from './Composer'
@@ -146,7 +146,7 @@ function ShellInner() {
dim: 'rgba(10, 13, 16, 0.8)',
}),
}}>
<TabsNavigator />
<NativeNavigator />
</Drawer>
</ErrorBoundary>
</View>