Account switcher (#85)
* Update the account-create and signin views to use the design system. Also: - Add borderDark to the theme - Start to an account selector in the signin flow * Dark mode fixes in signin ui * Track multiple active accounts and provide account-switching UI * Add test tooling for an in-memory pds * Add complete integration tests for login and the account switcher
This commit is contained in:
parent
439305b57e
commit
9027882fb4
23 changed files with 2406 additions and 658 deletions
|
@ -1,17 +1,21 @@
|
|||
import React, {useState} from 'react'
|
||||
import {
|
||||
SafeAreaView,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
useWindowDimensions,
|
||||
} from 'react-native'
|
||||
import Svg, {Line} from 'react-native-svg'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {Signin} from '../com/login/Signin'
|
||||
import {Logo} from '../com/login/Logo'
|
||||
import {CreateAccount} from '../com/login/CreateAccount'
|
||||
import {Text} from '../com/util/text/Text'
|
||||
import {ErrorBoundary} from '../com/util/ErrorBoundary'
|
||||
import {s, colors} from '../lib/styles'
|
||||
import {usePalette} from '../lib/hooks/usePalette'
|
||||
|
||||
enum ScreenState {
|
||||
SigninOrCreateAccount,
|
||||
|
@ -31,7 +35,7 @@ const SigninOrCreateAccount = ({
|
|||
return (
|
||||
<>
|
||||
<View style={styles.hero}>
|
||||
<Logo />
|
||||
<Logo color="white" />
|
||||
<Text style={styles.title}>Bluesky</Text>
|
||||
<Text style={styles.subtitle}>[ private beta ]</Text>
|
||||
</View>
|
||||
|
@ -76,40 +80,61 @@ const SigninOrCreateAccount = ({
|
|||
|
||||
export const Login = observer(
|
||||
(/*{navigation}: RootTabsScreenProps<'Login'>*/) => {
|
||||
const pal = usePalette('default')
|
||||
const [screenState, setScreenState] = useState<ScreenState>(
|
||||
ScreenState.SigninOrCreateAccount,
|
||||
)
|
||||
|
||||
if (screenState === ScreenState.SigninOrCreateAccount) {
|
||||
return (
|
||||
<LinearGradient
|
||||
colors={['#007CFF', '#00BCFF']}
|
||||
start={{x: 0, y: 0.8}}
|
||||
end={{x: 0, y: 1}}
|
||||
style={styles.container}>
|
||||
<SafeAreaView testID="noSessionView" style={styles.container}>
|
||||
<ErrorBoundary>
|
||||
<SigninOrCreateAccount
|
||||
onPressSignin={() => setScreenState(ScreenState.Signin)}
|
||||
onPressCreateAccount={() =>
|
||||
setScreenState(ScreenState.CreateAccount)
|
||||
}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</SafeAreaView>
|
||||
</LinearGradient>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.outer}>
|
||||
{screenState === ScreenState.SigninOrCreateAccount ? (
|
||||
<SigninOrCreateAccount
|
||||
onPressSignin={() => setScreenState(ScreenState.Signin)}
|
||||
onPressCreateAccount={() =>
|
||||
setScreenState(ScreenState.CreateAccount)
|
||||
}
|
||||
/>
|
||||
) : undefined}
|
||||
{screenState === ScreenState.Signin ? (
|
||||
<Signin
|
||||
onPressBack={() =>
|
||||
setScreenState(ScreenState.SigninOrCreateAccount)
|
||||
}
|
||||
/>
|
||||
) : undefined}
|
||||
{screenState === ScreenState.CreateAccount ? (
|
||||
<CreateAccount
|
||||
onPressBack={() =>
|
||||
setScreenState(ScreenState.SigninOrCreateAccount)
|
||||
}
|
||||
/>
|
||||
) : undefined}
|
||||
<View style={[styles.container, pal.view]}>
|
||||
<SafeAreaView testID="noSessionView" style={styles.container}>
|
||||
<ErrorBoundary>
|
||||
{screenState === ScreenState.Signin ? (
|
||||
<Signin
|
||||
onPressBack={() =>
|
||||
setScreenState(ScreenState.SigninOrCreateAccount)
|
||||
}
|
||||
/>
|
||||
) : undefined}
|
||||
{screenState === ScreenState.CreateAccount ? (
|
||||
<CreateAccount
|
||||
onPressBack={() =>
|
||||
setScreenState(ScreenState.SigninOrCreateAccount)
|
||||
}
|
||||
/>
|
||||
) : undefined}
|
||||
</ErrorBoundary>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
height: '100%',
|
||||
},
|
||||
outer: {
|
||||
flex: 1,
|
||||
},
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
import React, {useEffect} from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {useStores} from '../../state'
|
||||
import {ScreenParams} from '../routes'
|
||||
|
@ -7,8 +14,10 @@ import {s} from '../lib/styles'
|
|||
import {ViewHeader} from '../com/util/ViewHeader'
|
||||
import {Link} from '../com/util/Link'
|
||||
import {Text} from '../com/util/text/Text'
|
||||
import * as Toast from '../com/util/Toast'
|
||||
import {UserAvatar} from '../com/util/UserAvatar'
|
||||
import {usePalette} from '../lib/hooks/usePalette'
|
||||
import {AccountData} from '../../state/models/session'
|
||||
|
||||
export const Settings = observer(function Settings({
|
||||
navIdx,
|
||||
|
@ -16,6 +25,7 @@ export const Settings = observer(function Settings({
|
|||
}: ScreenParams) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const [isSwitching, setIsSwitching] = React.useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
|
@ -25,45 +35,114 @@ export const Settings = observer(function Settings({
|
|||
store.nav.setTitle(navIdx, 'Settings')
|
||||
}, [visible, store])
|
||||
|
||||
const onPressSwitchAccount = async (acct: AccountData) => {
|
||||
setIsSwitching(true)
|
||||
if (await store.session.resumeSession(acct)) {
|
||||
setIsSwitching(false)
|
||||
Toast.show(`Signed in as ${acct.displayName || acct.handle}`)
|
||||
return
|
||||
}
|
||||
setIsSwitching(false)
|
||||
Toast.show('Sorry! We need you to enter your password.')
|
||||
store.session.clear()
|
||||
}
|
||||
const onPressAddAccount = () => {
|
||||
store.session.clear()
|
||||
}
|
||||
const onPressSignout = () => {
|
||||
store.session.logout()
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[s.flex1]}>
|
||||
<View style={[s.h100pct]} testID="settingsScreen">
|
||||
<ViewHeader title="Settings" />
|
||||
<View style={[s.mt10, s.pl10, s.pr10, s.flex1]}>
|
||||
<ScrollView style={[s.mt10, s.pl10, s.pr10, s.h100pct]}>
|
||||
<View style={[s.flexRow]}>
|
||||
<Text type="xl" style={pal.text}>
|
||||
<Text type="xl-bold" style={pal.text}>
|
||||
Signed in as
|
||||
</Text>
|
||||
<View style={s.flex1} />
|
||||
<TouchableOpacity onPress={onPressSignout}>
|
||||
<TouchableOpacity
|
||||
testID="signOutBtn"
|
||||
onPress={isSwitching ? undefined : onPressSignout}>
|
||||
<Text type="xl-medium" style={pal.link}>
|
||||
Sign out
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Link
|
||||
href={`/profile/${store.me.handle}`}
|
||||
title="Your profile"
|
||||
noFeedback>
|
||||
{isSwitching ? (
|
||||
<View style={[pal.view, styles.profile]}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
) : (
|
||||
<Link
|
||||
href={`/profile/${store.me.handle}`}
|
||||
title="Your profile"
|
||||
noFeedback>
|
||||
<View style={[pal.view, styles.profile]}>
|
||||
<UserAvatar
|
||||
size={40}
|
||||
displayName={store.me.displayName}
|
||||
handle={store.me.handle || ''}
|
||||
avatar={store.me.avatar}
|
||||
/>
|
||||
<View style={[s.ml10]}>
|
||||
<Text type="xl-bold" style={pal.text}>
|
||||
{store.me.displayName || store.me.handle}
|
||||
</Text>
|
||||
<Text style={pal.textLight}>@{store.me.handle}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Link>
|
||||
)}
|
||||
<Text type="sm-medium" style={pal.text}>
|
||||
Switch to:
|
||||
</Text>
|
||||
{store.session.switchableAccounts.map(account => (
|
||||
<TouchableOpacity
|
||||
testID={`switchToAccountBtn-${account.handle}`}
|
||||
key={account.did}
|
||||
style={[
|
||||
pal.view,
|
||||
styles.profile,
|
||||
s.mb2,
|
||||
isSwitching && styles.dimmed,
|
||||
]}
|
||||
onPress={
|
||||
isSwitching ? undefined : () => onPressSwitchAccount(account)
|
||||
}>
|
||||
<UserAvatar
|
||||
size={40}
|
||||
displayName={store.me.displayName}
|
||||
handle={store.me.handle || ''}
|
||||
avatar={store.me.avatar}
|
||||
displayName={account.displayName}
|
||||
handle={account.handle || ''}
|
||||
avatar={account.aviUrl}
|
||||
/>
|
||||
<View style={[s.ml10]}>
|
||||
<Text type="xl-bold" style={pal.text}>
|
||||
{store.me.displayName || store.me.handle}
|
||||
{account.displayName || account.handle}
|
||||
</Text>
|
||||
<Text style={pal.textLight}>@{store.me.handle}</Text>
|
||||
<Text style={pal.textLight}>@{account.handle}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
<TouchableOpacity
|
||||
testID="switchToNewAccountBtn"
|
||||
style={[
|
||||
pal.view,
|
||||
styles.profile,
|
||||
s.mb2,
|
||||
{alignItems: 'center'},
|
||||
isSwitching && styles.dimmed,
|
||||
]}
|
||||
onPress={isSwitching ? undefined : onPressAddAccount}>
|
||||
<FontAwesomeIcon icon="plus" />
|
||||
<View style={[s.ml5]}>
|
||||
<Text type="md-medium" style={pal.text}>
|
||||
Add account
|
||||
</Text>
|
||||
</View>
|
||||
</Link>
|
||||
<View style={s.flex1} />
|
||||
</TouchableOpacity>
|
||||
<View style={{height: 50}} />
|
||||
<Text type="sm-medium" style={[s.mb5]}>
|
||||
Developer tools
|
||||
</Text>
|
||||
|
@ -80,12 +159,15 @@ export const Settings = observer(function Settings({
|
|||
<Text style={pal.link}>Storybook</Text>
|
||||
</Link>
|
||||
<View style={s.footerSpacer} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
dimmed: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
title: {
|
||||
fontSize: 32,
|
||||
fontWeight: 'bold',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue