Improvements to UI in web logged-out views (#1341)

* Add LoggedOutLayout for desktop/tablet web

* Avoid screen flash in the transition to onboarding

* Fix comment
This commit is contained in:
Paul Frazee 2023-08-30 17:55:01 -07:00 committed by GitHub
parent a498acab6e
commit 04992f14f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 239 additions and 120 deletions

View file

@ -109,13 +109,8 @@ export class CreateAccountModel {
this.setError('') this.setError('')
this.setIsProcessing(true) this.setIsProcessing(true)
// open the onboarding screens after the session is created
const sessionReadySub = this.rootStore.onSessionReady(() => {
sessionReadySub.remove()
this.rootStore.onboarding.start()
})
try { try {
this.rootStore.onboarding.start() // start now to avoid flashing the wrong view
await this.rootStore.session.createAccount({ await this.rootStore.session.createAccount({
service: this.serviceUrl, service: this.serviceUrl,
email: this.email, email: this.email,
@ -125,7 +120,7 @@ export class CreateAccountModel {
}) })
track('Create Account') track('Create Account')
} catch (e: any) { } catch (e: any) {
sessionReadySub.remove() this.rootStore.onboarding.skip() // undo starting the onboard
let errMsg = e.toString() let errMsg = e.toString()
if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) { if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) {
errMsg = errMsg =

View file

@ -9,7 +9,6 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {SplashScreen} from './SplashScreen' import {SplashScreen} from './SplashScreen'
import {CenteredView} from '../util/Views'
enum ScreenState { enum ScreenState {
S_LoginOrCreateAccount, S_LoginOrCreateAccount,
@ -43,25 +42,23 @@ export const LoggedOut = observer(() => {
} }
return ( return (
<CenteredView style={[s.hContentRegion, pal.view]}> <SafeAreaView testID="noSessionView" style={[s.hContentRegion, pal.view]}>
<SafeAreaView testID="noSessionView" style={s.hContentRegion}> <ErrorBoundary>
<ErrorBoundary> {screenState === ScreenState.S_Login ? (
{screenState === ScreenState.S_Login ? ( <Login
<Login onPressBack={() =>
onPressBack={() => setScreenState(ScreenState.S_LoginOrCreateAccount)
setScreenState(ScreenState.S_LoginOrCreateAccount) }
} />
/> ) : undefined}
) : undefined} {screenState === ScreenState.S_CreateAccount ? (
{screenState === ScreenState.S_CreateAccount ? ( <CreateAccount
<CreateAccount onPressBack={() =>
onPressBack={() => setScreenState(ScreenState.S_LoginOrCreateAccount)
setScreenState(ScreenState.S_LoginOrCreateAccount) }
} />
/> ) : undefined}
) : undefined} </ErrorBoundary>
</ErrorBoundary> </SafeAreaView>
</SafeAreaView>
</CenteredView>
) )
}) })

View file

@ -10,6 +10,7 @@ import {
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {Text} from '../../util/text/Text' import {Text} from '../../util/text/Text'
import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {CreateAccountModel} from 'state/models/ui/create-account' import {CreateAccountModel} from 'state/models/ui/create-account'
@ -65,60 +66,65 @@ export const CreateAccount = observer(
}, [model, track]) }, [model, track])
return ( return (
<ScrollView testID="createAccount" style={pal.view}> <LoggedOutLayout
<KeyboardAvoidingView behavior="padding"> leadin={`Step ${model.step}`}
<View style={styles.stepContainer}> title="Create Account"
{model.step === 1 && <Step1 model={model} />} description="We're so excited to have you join us!">
{model.step === 2 && <Step2 model={model} />} <ScrollView testID="createAccount" style={pal.view}>
{model.step === 3 && <Step3 model={model} />} <KeyboardAvoidingView behavior="padding">
</View> <View style={styles.stepContainer}>
<View style={[s.flexRow, s.pl20, s.pr20]}> {model.step === 1 && <Step1 model={model} />}
<TouchableOpacity {model.step === 2 && <Step2 model={model} />}
onPress={onPressBackInner} {model.step === 3 && <Step3 model={model} />}
testID="backBtn" </View>
accessibilityRole="button"> <View style={[s.flexRow, s.pl20, s.pr20]}>
<Text type="xl" style={pal.link}>
Back
</Text>
</TouchableOpacity>
<View style={s.flex1} />
{model.canNext ? (
<TouchableOpacity <TouchableOpacity
testID="nextBtn" onPress={onPressBackInner}
onPress={onPressNext} testID="backBtn"
accessibilityRole="button"> accessibilityRole="button">
{model.isProcessing ? ( <Text type="xl" style={pal.link}>
<ActivityIndicator /> Back
) : ( </Text>
</TouchableOpacity>
<View style={s.flex1} />
{model.canNext ? (
<TouchableOpacity
testID="nextBtn"
onPress={onPressNext}
accessibilityRole="button">
{model.isProcessing ? (
<ActivityIndicator />
) : (
<Text type="xl-bold" style={[pal.link, s.pr5]}>
Next
</Text>
)}
</TouchableOpacity>
) : model.didServiceDescriptionFetchFail ? (
<TouchableOpacity
testID="retryConnectBtn"
onPress={onPressRetryConnect}
accessibilityRole="button"
accessibilityLabel="Retry"
accessibilityHint="Retries account creation"
accessibilityLiveRegion="polite">
<Text type="xl-bold" style={[pal.link, s.pr5]}> <Text type="xl-bold" style={[pal.link, s.pr5]}>
Next Retry
</Text> </Text>
)} </TouchableOpacity>
</TouchableOpacity> ) : model.isFetchingServiceDescription ? (
) : model.didServiceDescriptionFetchFail ? ( <>
<TouchableOpacity <ActivityIndicator color="#fff" />
testID="retryConnectBtn" <Text type="xl" style={[pal.text, s.pr5]}>
onPress={onPressRetryConnect} Connecting...
accessibilityRole="button" </Text>
accessibilityLabel="Retry" </>
accessibilityHint="Retries account creation" ) : undefined}
accessibilityLiveRegion="polite"> </View>
<Text type="xl-bold" style={[pal.link, s.pr5]}> <View style={s.footerSpacer} />
Retry </KeyboardAvoidingView>
</Text> </ScrollView>
</TouchableOpacity> </LoggedOutLayout>
) : model.isFetchingServiceDescription ? (
<>
<ActivityIndicator color="#fff" />
<Text type="xl" style={[pal.text, s.pr5]}>
Connecting...
</Text>
</>
) : undefined}
</View>
<View style={s.footerSpacer} />
</KeyboardAvoidingView>
</ScrollView>
) )
}, },
) )

View file

@ -93,6 +93,7 @@ function validWebLink(url?: string): string | undefined {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
policies: { policies: {
flexDirection: 'row',
gap: 8, gap: 8,
}, },
errorIcon: { errorIcon: {

View file

@ -17,6 +17,7 @@ import {BskyAgent} from '@atproto/api'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {Text} from '../../util/text/Text' import {Text} from '../../util/text/Text'
import {UserAvatar} from '../../util/UserAvatar' import {UserAvatar} from '../../util/UserAvatar'
import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout'
import {s, colors} from 'lib/styles' import {s, colors} from 'lib/styles'
import {createFullHandle} from 'lib/strings/handles' import {createFullHandle} from 'lib/strings/handles'
import {toNiceDomain} from 'lib/strings/url-helpers' import {toNiceDomain} from 'lib/strings/url-helpers'
@ -99,52 +100,69 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
} }
return ( return (
<KeyboardAvoidingView <KeyboardAvoidingView testID="signIn" behavior="padding" style={pal.view}>
testID="signIn"
behavior="padding"
style={[pal.view, s.pt10]}>
{currentForm === Forms.Login ? ( {currentForm === Forms.Login ? (
<LoginForm <LoggedOutLayout
store={store} leadin=""
error={error} title="Sign in"
serviceUrl={serviceUrl} description="Enter your username and password">
serviceDescription={serviceDescription} <LoginForm
initialHandle={initialHandle} store={store}
setError={setError} error={error}
setServiceUrl={setServiceUrl} serviceUrl={serviceUrl}
onPressBack={onPressBack} serviceDescription={serviceDescription}
onPressForgotPassword={onPressForgotPassword} initialHandle={initialHandle}
onPressRetryConnect={onPressRetryConnect} setError={setError}
/> setServiceUrl={setServiceUrl}
onPressBack={onPressBack}
onPressForgotPassword={onPressForgotPassword}
onPressRetryConnect={onPressRetryConnect}
/>
</LoggedOutLayout>
) : undefined} ) : undefined}
{currentForm === Forms.ChooseAccount ? ( {currentForm === Forms.ChooseAccount ? (
<ChooseAccountForm <LoggedOutLayout
store={store} leadin=""
onSelectAccount={onSelectAccount} title="Sign in as..."
onPressBack={onPressBack} description="Select from an existing account">
/> <ChooseAccountForm
store={store}
onSelectAccount={onSelectAccount}
onPressBack={onPressBack}
/>
</LoggedOutLayout>
) : undefined} ) : undefined}
{currentForm === Forms.ForgotPassword ? ( {currentForm === Forms.ForgotPassword ? (
<ForgotPasswordForm <LoggedOutLayout
store={store} leadin=""
error={error} title="Forgot Password"
serviceUrl={serviceUrl} description="Let's get your password reset!">
serviceDescription={serviceDescription} <ForgotPasswordForm
setError={setError} store={store}
setServiceUrl={setServiceUrl} error={error}
onPressBack={gotoForm(Forms.Login)} serviceUrl={serviceUrl}
onEmailSent={gotoForm(Forms.SetNewPassword)} serviceDescription={serviceDescription}
/> setError={setError}
setServiceUrl={setServiceUrl}
onPressBack={gotoForm(Forms.Login)}
onEmailSent={gotoForm(Forms.SetNewPassword)}
/>
</LoggedOutLayout>
) : undefined} ) : undefined}
{currentForm === Forms.SetNewPassword ? ( {currentForm === Forms.SetNewPassword ? (
<SetNewPasswordForm <LoggedOutLayout
store={store} leadin=""
error={error} title="Forgot Password"
serviceUrl={serviceUrl} description="Let's get your password reset!">
setError={setError} <SetNewPasswordForm
onPressBack={gotoForm(Forms.ForgotPassword)} store={store}
onPasswordSet={gotoForm(Forms.PasswordUpdated)} error={error}
/> serviceUrl={serviceUrl}
setError={setError}
onPressBack={gotoForm(Forms.ForgotPassword)}
onPasswordSet={gotoForm(Forms.PasswordUpdated)}
/>
</LoggedOutLayout>
) : undefined} ) : undefined}
{currentForm === Forms.PasswordUpdated ? ( {currentForm === Forms.PasswordUpdated ? (
<PasswordUpdatedForm onPressNext={gotoForm(Forms.Login)} /> <PasswordUpdatedForm onPressNext={gotoForm(Forms.Login)} />
@ -834,9 +852,9 @@ const SetNewPasswordForm = ({
const PasswordUpdatedForm = ({onPressNext}: {onPressNext: () => void}) => { const PasswordUpdatedForm = ({onPressNext}: {onPressNext: () => void}) => {
const {screen} = useAnalytics() const {screen} = useAnalytics()
// useEffect(() => { useEffect(() => {
screen('Signin:PasswordUpdatedForm') screen('Signin:PasswordUpdatedForm')
// }, [screen]) }, [screen])
const pal = usePalette('default') const pal = usePalette('default')
return ( return (

View file

@ -0,0 +1,102 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {Text} from '../text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
export const LoggedOutLayout = ({
leadin,
title,
description,
children,
}: React.PropsWithChildren<{
leadin: string
title: string
description: string
}>) => {
const {isMobile, isTabletOrMobile} = useWebMediaQueries()
const pal = usePalette('default')
const sideBg = useColorSchemeStyle(pal.viewLight, pal.view)
const contentBg = useColorSchemeStyle(pal.view, {
backgroundColor: pal.colors.background,
borderColor: pal.colors.border,
borderLeftWidth: 1,
})
if (isMobile) {
return <View style={{paddingTop: 10}}>{children}</View>
}
return (
<View style={styles.container}>
<View style={[styles.side, sideBg]}>
<Text
style={[
pal.textLight,
styles.leadinText,
isTabletOrMobile && styles.leadinTextSmall,
]}>
{leadin}
</Text>
<Text
style={[
pal.link,
styles.titleText,
isTabletOrMobile && styles.titleTextSmall,
]}>
{title}
</Text>
<Text type="2xl-medium" style={[pal.textLight, styles.descriptionText]}>
{description}
</Text>
</View>
<View style={[styles.content, contentBg]}>
<View style={styles.contentWrapper}>{children}</View>
</View>
</View>
)
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
height: '100vh',
},
side: {
flex: 1,
paddingHorizontal: 40,
paddingBottom: 80,
justifyContent: 'center',
},
content: {
flex: 2,
paddingHorizontal: 40,
justifyContent: 'center',
},
leadinText: {
fontSize: 36,
fontWeight: '800',
textAlign: 'right',
},
leadinTextSmall: {
fontSize: 24,
},
titleText: {
fontSize: 58,
fontWeight: '800',
textAlign: 'right',
},
titleTextSmall: {
fontSize: 36,
},
descriptionText: {
maxWidth: 400,
marginTop: 10,
marginLeft: 'auto',
textAlign: 'right',
},
contentWrapper: {
maxWidth: 600,
},
})