move auth screens into the navigation system
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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') {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user