new approach, new design

This commit is contained in:
Samuel Newman
2025-07-08 17:07:34 +03:00
parent c71a76fa2e
commit e5613c909c
17 changed files with 335 additions and 25 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

@@ -2,11 +2,15 @@ import {
type AuthNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
import * as Layout from './components/Layout'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'ForgotPassword'>
export function ForgotPasswordScreen({}: Props) {
return <ForgotPasswordScreenInner />
}
export function ForgotPasswordScreenInner() {
return (
<Layout.Screen testID="ForgotPasswordScreen">
<Layout.Header.Outer noBottomBorder>
+27 -6
View File
@@ -7,7 +7,8 @@ import {
type AuthNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {useSession} from '#/state/session'
import {isNative} from '#/platform/detection'
import {type SessionAccount, useSession} from '#/state/session'
import {
useLoggedOutView,
useLoggedOutViewControls,
@@ -18,11 +19,29 @@ 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'
import * as Layout from './components/Layout'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'Landing'>
export function LandingScreen({navigation}: Props) {
return (
<LandingScreenInner
signUp={() => navigation.navigate('SignUpInfo')}
selectAccount={() => navigation.navigate('SelectAccount')}
signIn={account => navigation.navigate('SignIn', {account})}
/>
)
}
export function LandingScreenInner({
signUp,
selectAccount,
signIn,
}: {
signUp: () => void
selectAccount: () => void
signIn: (account?: SessionAccount) => void
}) {
const {_} = useLingui()
const t = useTheme()
const insets = useSafeAreaInsets()
@@ -38,7 +57,7 @@ export function LandingScreen({navigation}: Props) {
<Layout.Screen
testID="LandingScreen"
style={{paddingBottom: insets.bottom}}>
{showLoggedOut && (
{isNative && showLoggedOut && (
<Layout.Header.Outer noBottomBorder>
<Layout.Header.Slot />
<Layout.Header.Content />
@@ -71,7 +90,7 @@ export function LandingScreen({navigation}: Props) {
style={[a.px_xl, a.gap_md, a.pb_2xl]}>
<Button
testID="createAccountButton"
onPress={() => navigation.push('SignUpInfo')}
onPress={signUp}
label={_(msg`Create new account`)}
size="large"
variant="solid"
@@ -84,9 +103,11 @@ export function LandingScreen({navigation}: Props) {
testID="signInButton"
onPress={() => {
if (requestedAccount) {
navigation.push('SignIn', {account: requestedAccount})
signIn(requestedAccount)
} else if (accounts.length > 0) {
selectAccount()
} else {
navigation.push(accounts.length > 0 ? 'SelectAccount' : 'SignIn')
signIn()
}
}}
label={_(msg`Sign in`)}
@@ -2,11 +2,15 @@ import {
type AuthNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
import * as Layout from './components/Layout'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'PasswordUpdated'>
export function PasswordUpdatedScreen({}: Props) {
return <PasswordUpdatedScreenInner />
}
export function PasswordUpdatedScreenInner() {
return (
<Layout.Screen testID="PasswordUpdatedScreen">
<Layout.Header.Outer noBottomBorder>
@@ -10,13 +10,25 @@ 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 {atoms as a} from '#/alf'
import {AccountList} from '#/components/AccountList'
import * as TextField from '#/components/forms/TextField'
import * as Layout from '#/components/Layout'
import * as Layout from './components/Layout'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'SelectAccount'>
export function SelectAccountScreen({navigation}: Props) {
return (
<SelectAccountScreenInner
signIn={account => navigation.navigate('SignIn', {account})}
/>
)
}
export function SelectAccountScreenInner({
signIn,
}: {
signIn: (account?: SessionAccount) => void
}) {
const [pendingDid, setPendingDid] = useState<string | null>(null)
const {_} = useLingui()
const {currentAccount} = useSession()
@@ -30,7 +42,7 @@ export function SelectAccountScreen({navigation}: Props) {
}
if (!account.accessJwt) {
// Move to login form.
navigation.push('SignIn', {account})
signIn(account)
return
}
if (account.did === currentAccount?.did) {
@@ -50,7 +62,7 @@ export function SelectAccountScreen({navigation}: Props) {
message: e.message,
})
// Move to login form.
navigation.push('SignIn', {account})
signIn(account)
} finally {
setPendingDid(null)
}
@@ -63,13 +75,13 @@ export function SelectAccountScreen({navigation}: Props) {
<Layout.Header.Content />
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content contentContainerStyle={[a.py_xl, native(a.px_xl)]}>
<Layout.Content contentContainerStyle={[a.p_xl]}>
<TextField.LabelText>
<Trans>Sign in as...</Trans>
</TextField.LabelText>
<AccountList
onSelectAccount={onSelect}
onSelectOther={() => navigation.push('SignIn')}
onSelectOther={signIn}
pendingDid={pendingDid}
/>
</Layout.Content>
@@ -2,11 +2,15 @@ import {
type AuthNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
import * as Layout from './components/Layout'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'SetNewPassword'>
export function SetNewPasswordScreen({}: Props) {
return <SetNewPasswordScreenInner />
}
export function SetNewPasswordScreenInner() {
return (
<Layout.Screen testID="SetNewPasswordScreen">
<Layout.Header.Outer noBottomBorder>
+22 -3
View File
@@ -9,9 +9,10 @@ import {
} from '#/lib/routes/types'
import {logger} from '#/logger'
import {useServiceQuery} from '#/state/queries/service'
import {type SessionAccount} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
import * as Layout from './components/Layout'
import {SignInForm} from './components/SignInForm'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'SignIn'>
@@ -19,6 +20,24 @@ export function SignInScreen({
navigation,
route: {params: {account: requestedAccount} = {}},
}: Props) {
return (
<SignInScreenInner
requestedAccount={requestedAccount}
forgotPassword={() => navigation.navigate('ForgotPassword')}
signUp={() => navigation.navigate('SignUpInfo')}
/>
)
}
export function SignInScreenInner({
requestedAccount,
forgotPassword,
signUp,
}: {
requestedAccount?: SessionAccount
forgotPassword: () => void
signUp: () => void
}) {
const t = useTheme()
const {_} = useLingui()
const failedAttemptCountRef = useRef(0)
@@ -51,7 +70,7 @@ export function SignInScreen({
}, [serviceError, serviceUrl, _])
const onPressForgotPassword = () => {
navigation.push('ForgotPassword')
forgotPassword()
logger.metric('signin:forgotPasswordPressed', {})
}
@@ -93,7 +112,7 @@ export function SignInScreen({
New to Bluesky?{' '}
<Text
role="link"
onPress={() => navigation.push('SignUpInfo')}
onPress={signUp}
style={[a.text_md, {color: t.palette.primary_500}]}>
Create account
</Text>
@@ -2,11 +2,15 @@ import {
type AuthNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
import * as Layout from './components/Layout'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'SignUpCaptcha'>
export function SignUpCaptchaScreen({}: Props) {
return <SignUpCaptchaScreenInner />
}
export function SignUpCaptchaScreenInner() {
return (
<Layout.Screen testID="SignUpCaptchaScreen">
<Layout.Header.Outer noBottomBorder>
@@ -2,11 +2,15 @@ import {
type AuthNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
import * as Layout from './components/Layout'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'SignUpHandle'>
export function SignUpHandleScreen({}: Props) {
return <SignUpHandleScreenInner />
}
export function SignUpHandleScreenInner() {
return (
<Layout.Screen testID="SignUpHandleScreen">
<Layout.Header.Outer noBottomBorder>
@@ -2,11 +2,15 @@ import {
type AuthNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
import * as Layout from './components/Layout'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'SignUpInfo'>
export function SignUpInfoScreen({}: Props) {
return <SignUpInfoScreenInner />
}
export function SignUpInfoScreenInner() {
return (
<Layout.Screen testID="SignUpInfoScreen">
<Layout.Header.Outer noBottomBorder>
@@ -2,11 +2,15 @@ import {
type AuthNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'
import * as Layout from './components/Layout'
type Props = NativeStackScreenProps<AuthNavigatorParams, 'StarterPackLanding'>
export function StarterPackLandingScreen({}: Props) {
return <StarterPackLandingScreenInner />
}
export function StarterPackLandingScreenInner() {
return (
<Layout.Screen testID="StarterPackLandingScreen">
<Layout.Content>
@@ -0,0 +1,5 @@
export function AuthenticationFlow() {
throw new Error(
'AuthenticationFlow is only available on web. Navigate to the auth screens instead.',
)
}
@@ -0,0 +1,203 @@
import {useReducer} from 'react'
import {View} from 'react-native'
import {ImageBackground} from 'expo-image'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {type SessionAccount} from '#/state/session'
import {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype'
import {atoms as a, tokens, useTheme, web} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import {ForgotPasswordScreenInner} from '../../ForgotPasswordScreen'
import {LandingScreenInner} from '../../LandingScreen'
import {PasswordUpdatedScreenInner} from '../../PasswordUpdatedScreen'
import {SelectAccountScreenInner} from '../../SelectAccountScreen'
import {SetNewPasswordScreenInner} from '../../SetNewPasswordScreen'
import {SignInScreenInner} from '../../SignInScreen'
import {SignUpCaptchaScreenInner} from '../../SignUpCaptchaScreen'
import {SignUpHandleScreenInner} from '../../SignUpHandleScreen'
import {SignUpInfoScreenInner} from '../../SignUpInfoScreen'
import {StarterPackLandingScreenInner} from '../../StarterPackLandingScreen'
type State =
| {
screen: 'Landing'
}
| {
screen: 'ForgotPassword'
}
| {
screen: 'PasswordUpdated'
}
| {
screen: 'SelectAccount'
}
| {
screen: 'SetNewPassword'
}
| {
screen: 'SignIn'
requestedAccount?: SessionAccount
}
| {
screen: 'SignUpCaptcha'
}
| {
screen: 'SignUpHandle'
}
| {
screen: 'SignUpInfo'
}
| {
screen: 'StarterPackLanding'
}
type Action =
| {
type: 'NAVIGATE_TO_SIGN_IN'
account?: SessionAccount
}
| {
type: 'NAVIGATE_TO_SIGN_UP'
}
| {
type: 'NAVIGATE_TO_SELECT_ACCOUNT'
}
| {
type: 'NAVIGATE_TO_FORGOT_PASSWORD'
}
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'NAVIGATE_TO_SIGN_IN':
return {screen: 'SignIn', requestedAccount: action.account}
case 'NAVIGATE_TO_SIGN_UP':
return {screen: 'SignUpHandle'}
case 'NAVIGATE_TO_SELECT_ACCOUNT':
return {screen: 'SelectAccount'}
case 'NAVIGATE_TO_FORGOT_PASSWORD':
return {screen: 'ForgotPassword'}
default:
// @ts-expect-error this will stop throwing an error if we miss a case
throw new Error(`Unhandled action type: ${action.type}`)
}
}
export function AuthenticationFlow({onDismiss}: {onDismiss?: () => void}) {
const [state, dispatch] = useReducer(reducer, {screen: 'Landing'})
let content
switch (state.screen) {
case 'ForgotPassword':
content = <ForgotPasswordScreenInner />
break
case 'Landing':
content = (
<LandingScreenInner
signIn={account => dispatch({type: 'NAVIGATE_TO_SIGN_IN', account})}
selectAccount={() => dispatch({type: 'NAVIGATE_TO_SELECT_ACCOUNT'})}
signUp={() => dispatch({type: 'NAVIGATE_TO_SIGN_UP'})}
/>
)
break
case 'PasswordUpdated':
content = <PasswordUpdatedScreenInner />
break
case 'SelectAccount':
content = (
<SelectAccountScreenInner
signIn={account => dispatch({type: 'NAVIGATE_TO_SIGN_IN', account})}
/>
)
break
case 'SetNewPassword':
content = <SetNewPasswordScreenInner />
break
case 'SignIn':
content = (
<SignInScreenInner
requestedAccount={state.requestedAccount}
signUp={() => dispatch({type: 'NAVIGATE_TO_SIGN_UP'})}
forgotPassword={() => dispatch({type: 'NAVIGATE_TO_FORGOT_PASSWORD'})}
/>
)
break
case 'SignUpCaptcha':
content = <SignUpCaptchaScreenInner />
break
case 'SignUpHandle':
content = <SignUpHandleScreenInner />
break
case 'SignUpInfo':
content = <SignUpInfoScreenInner />
break
case 'StarterPackLanding':
content = <StarterPackLandingScreenInner />
break
}
return <Shell onDismiss={onDismiss}>{content}</Shell>
}
function Shell({
children,
onDismiss,
}: {
children?: React.ReactNode
onDismiss?: () => void
}) {
const t = useTheme()
const {_} = useLingui()
const image =
t.scheme === 'light'
? require('../../../../../assets/background-light.webp')
: require('../../../../../assets/background-dark.webp')
return (
<ImageBackground
style={[
a.w_full,
a.h_full,
web({minHeight: '100vh'}),
a.align_center,
a.justify_center,
a.gap_md,
a.px_md,
]}
source={image}>
{onDismiss && (
<Button
label={_(msg`Close`)}
onPress={onDismiss}
variant="solid"
color="secondary_inverted"
shape="round"
size="small"
style={[a.absolute, {top: tokens.space.xl, right: tokens.space.xl}]}>
<ButtonIcon icon={XIcon} />
</Button>
)}
<View
style={[a.px_lg, a.justify_center, a.align_center, {height: '20%'}]}>
<View style={[a.align_end, a.flex_row, a.gap_xl]}>
<Logo fill="white" width={65} />
<Logotype fill="white" width={180} />
</View>
</View>
<View
style={[
a.rounded_lg,
t.atoms.bg,
web({height: 440, maxHeight: 'calc(60% - 32px)'}),
a.w_full,
{maxWidth: 480},
a.overflow_auto,
]}>
{children}
</View>
</ImageBackground>
)
}
@@ -0,0 +1 @@
export * from '#/components/Layout'
@@ -0,0 +1,21 @@
import {memo} from 'react'
import {View} from 'react-native'
import {atoms as a} from '#/alf'
import {type ContentProps, type ScreenProps} from '#/components/Layout'
export * as Header from '#/components/Layout/Header'
export const Screen = memo(function Screen({style, ...props}: ScreenProps) {
return <View style={[a.w_full, a.h_full, style]} {...props} />
})
export const Content = memo(function Content({
style,
contentContainerStyle,
children,
}: React.PropsWithChildren<ContentProps>) {
return (
<View style={[a.flex_1, style, contentContainerStyle]}>{children}</View>
)
})
@@ -34,7 +34,7 @@ import {
useLoggedOutView,
useLoggedOutViewControls,
} from '#/state/shell/logged-out'
import {LoggedOut} from '#/view/com/auth/LoggedOut'
import {AuthenticationFlow} from '#/screens/Authentication/components/Flow'
import {Deactivated} from '#/screens/Deactivated'
import {Onboarding} from '#/screens/Onboarding'
import {SignupQueued} from '#/screens/SignupQueued'
@@ -114,7 +114,7 @@ function NativeStackNavigator({
const {leftNavMinimal} = useLayoutBreakpoints()
// Temp: use old system for web. TODO: unify
if (isWeb && !hasSession && (!PWI_ENABLED || activeRouteRequiresAuth)) {
return <LoggedOut />
return <AuthenticationFlow />
}
if (hasSession && currentAccount?.signupQueued) {
return <SignupQueued />
@@ -123,7 +123,7 @@ function NativeStackNavigator({
return <Takendown />
}
if (isWeb && showLoggedOut) {
return <LoggedOut onDismiss={() => setShowLoggedOut(false)} />
return <AuthenticationFlow onDismiss={() => setShowLoggedOut(false)} />
}
if (currentAccount?.status === 'deactivated') {
return <Deactivated />