move auth screens into the navigation system
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
|||||||
NavigationContainer,
|
NavigationContainer,
|
||||||
StackActions,
|
StackActions,
|
||||||
} from '@react-navigation/native'
|
} from '@react-navigation/native'
|
||||||
|
import {createNativeStackNavigator} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
import {timeout} from '#/lib/async/timeout'
|
import {timeout} from '#/lib/async/timeout'
|
||||||
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
|
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
|
||||||
@@ -30,6 +31,7 @@ import {buildStateObject} from '#/lib/routes/helpers'
|
|||||||
import {
|
import {
|
||||||
type AllNavigatorParams,
|
type AllNavigatorParams,
|
||||||
type BottomTabNavigatorParams,
|
type BottomTabNavigatorParams,
|
||||||
|
type CoreNavigatorParams,
|
||||||
type FlatNavigatorParams,
|
type FlatNavigatorParams,
|
||||||
type HomeTabNavigatorParams,
|
type HomeTabNavigatorParams,
|
||||||
type MessagesTabNavigatorParams,
|
type MessagesTabNavigatorParams,
|
||||||
@@ -71,6 +73,16 @@ import {SupportScreen} from '#/view/screens/Support'
|
|||||||
import {TermsOfServiceScreen} from '#/view/screens/TermsOfService'
|
import {TermsOfServiceScreen} from '#/view/screens/TermsOfService'
|
||||||
import {BottomBar} from '#/view/shell/bottom-bar/BottomBar'
|
import {BottomBar} from '#/view/shell/bottom-bar/BottomBar'
|
||||||
import {createNativeStackNavigatorWithAuth} from '#/view/shell/createNativeStackNavigatorWithAuth'
|
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 {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTesterScreen'
|
||||||
import HashtagScreen from '#/screens/Hashtag'
|
import HashtagScreen from '#/screens/Hashtag'
|
||||||
import {MessagesScreen} from '#/screens/Messages/ChatList'
|
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 {useAccountSwitcher} from './lib/hooks/useAccountSwitcher'
|
||||||
import {useNonReactiveCallback} from './lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from './lib/hooks/useNonReactiveCallback'
|
||||||
import {useLoggedOutViewControls} from './state/shell/logged-out'
|
import {useLoggedOutViewControls} from './state/shell/logged-out'
|
||||||
|
import {useLoggedOutView} from './state/shell/logged-out'
|
||||||
import {useCloseAllActiveElements} from './state/util'
|
import {useCloseAllActiveElements} from './state/util'
|
||||||
|
|
||||||
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
||||||
|
|
||||||
|
const Core = createNativeStackNavigator<CoreNavigatorParams>()
|
||||||
const HomeTab = createNativeStackNavigatorWithAuth<HomeTabNavigatorParams>()
|
const HomeTab = createNativeStackNavigatorWithAuth<HomeTabNavigatorParams>()
|
||||||
const SearchTab = createNativeStackNavigatorWithAuth<SearchTabNavigatorParams>()
|
const SearchTab = createNativeStackNavigatorWithAuth<SearchTabNavigatorParams>()
|
||||||
const NotificationsTab =
|
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
|
* The RoutesContainer should wrap all components which need access
|
||||||
* to the navigation context.
|
* to the navigation context.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {type NavigationState, type PartialState} from '@react-navigation/native'
|
import {type NavigationState, type PartialState} from '@react-navigation/native'
|
||||||
import {type NativeStackNavigationProp} from '@react-navigation/native-stack'
|
import {type NativeStackNavigationProp} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
|
import {type SessionAccount} from '#/state/session'
|
||||||
import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types'
|
import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types'
|
||||||
|
|
||||||
export type {NativeStackScreenProps} from '@react-navigation/native-stack'
|
export type {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
@@ -133,6 +134,33 @@ export type AllNavigatorParams = CommonNavigatorParams & {
|
|||||||
Messages: {animation?: 'push' | 'pop'}
|
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
|
// NOTE
|
||||||
// this isn't strictly correct but it should be close enough
|
// this isn't strictly correct but it should be close enough
|
||||||
// a TS wizard might be able to get this 100%
|
// 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 {type ComponentProps} from 'react'
|
||||||
import {StyleSheet, TouchableWithoutFeedback} from 'react-native'
|
import {StyleSheet} from 'react-native'
|
||||||
import Animated from 'react-native-reanimated'
|
import Animated from 'react-native-reanimated'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {LinearGradient} from 'expo-linear-gradient'
|
import {LinearGradient} from 'expo-linear-gradient'
|
||||||
@@ -13,8 +13,7 @@ import {gradients} from '#/lib/styles'
|
|||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {ios} from '#/alf'
|
import {ios} from '#/alf'
|
||||||
|
|
||||||
export interface FABProps
|
export interface FABProps extends ComponentProps<typeof PressableScale> {
|
||||||
extends ComponentProps<typeof TouchableWithoutFeedback> {
|
|
||||||
testID?: string
|
testID?: string
|
||||||
icon: JSX.Element
|
icon: JSX.Element
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
|
|
||||||
import {PWI_ENABLED} from '#/lib/build-flags'
|
import {PWI_ENABLED} from '#/lib/build-flags'
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
import {isNative, isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useOnboardingState} from '#/state/shell'
|
import {useOnboardingState} from '#/state/shell'
|
||||||
import {
|
import {
|
||||||
@@ -112,7 +112,8 @@ function NativeStackNavigator({
|
|||||||
const {setShowLoggedOut} = useLoggedOutViewControls()
|
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
const {leftNavMinimal} = useLayoutBreakpoints()
|
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 />
|
return <LoggedOut />
|
||||||
}
|
}
|
||||||
if (hasSession && currentAccount?.signupQueued) {
|
if (hasSession && currentAccount?.signupQueued) {
|
||||||
@@ -121,7 +122,7 @@ function NativeStackNavigator({
|
|||||||
if (hasSession && currentAccount?.status === 'takendown') {
|
if (hasSession && currentAccount?.status === 'takendown') {
|
||||||
return <Takendown />
|
return <Takendown />
|
||||||
}
|
}
|
||||||
if (showLoggedOut) {
|
if (isWeb && showLoggedOut) {
|
||||||
return <LoggedOut onDismiss={() => setShowLoggedOut(false)} />
|
return <LoggedOut onDismiss={() => setShowLoggedOut(false)} />
|
||||||
}
|
}
|
||||||
if (currentAccount?.status === 'deactivated') {
|
if (currentAccount?.status === 'deactivated') {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {LinkWarningDialog} from '#/components/dialogs/LinkWarning'
|
|||||||
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
||||||
import {SigninDialog} from '#/components/dialogs/Signin'
|
import {SigninDialog} from '#/components/dialogs/Signin'
|
||||||
import {Outlet as PortalOutlet} from '#/components/Portal'
|
import {Outlet as PortalOutlet} from '#/components/Portal'
|
||||||
import {RoutesContainer, TabsNavigator} from '#/Navigation'
|
import {NativeNavigator, RoutesContainer} from '#/Navigation'
|
||||||
import {BottomSheetOutlet} from '../../../modules/bottom-sheet'
|
import {BottomSheetOutlet} from '../../../modules/bottom-sheet'
|
||||||
import {updateActiveViewAsync} from '../../../modules/expo-bluesky-swiss-army/src/VisibilityView'
|
import {updateActiveViewAsync} from '../../../modules/expo-bluesky-swiss-army/src/VisibilityView'
|
||||||
import {Composer} from './Composer'
|
import {Composer} from './Composer'
|
||||||
@@ -146,7 +146,7 @@ function ShellInner() {
|
|||||||
dim: 'rgba(10, 13, 16, 0.8)',
|
dim: 'rgba(10, 13, 16, 0.8)',
|
||||||
}),
|
}),
|
||||||
}}>
|
}}>
|
||||||
<TabsNavigator />
|
<NativeNavigator />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
Reference in New Issue
Block a user