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
|
@ -12,7 +12,7 @@ import {
|
|||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {ComAtprotoAccountCreate} from '@atproto/api'
|
||||
import * as EmailValidator from 'email-validator'
|
||||
import {Logo} from './Logo'
|
||||
import {LogoTextHero} from './Logo'
|
||||
import {Picker} from '../util/Picker'
|
||||
import {TextLink} from '../util/Link'
|
||||
import {Text} from '../util/text/Text'
|
||||
|
@ -25,8 +25,10 @@ import {
|
|||
import {useStores, DEFAULT_SERVICE} from '../../../state'
|
||||
import {ServiceDescription} from '../../../state/models/session'
|
||||
import {ServerInputModal} from '../../../state/models/shell-ui'
|
||||
import {usePalette} from '../../lib/hooks/usePalette'
|
||||
|
||||
export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||
const [serviceUrl, setServiceUrl] = useState<string>(DEFAULT_SERVICE)
|
||||
|
@ -114,74 +116,14 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
}
|
||||
}
|
||||
|
||||
const Policies = () => {
|
||||
if (!serviceDescription) {
|
||||
return <View />
|
||||
}
|
||||
const tos = validWebLink(serviceDescription.links?.termsOfService)
|
||||
const pp = validWebLink(serviceDescription.links?.privacyPolicy)
|
||||
if (!tos && !pp) {
|
||||
return (
|
||||
<View style={styles.policies}>
|
||||
<View style={[styles.errorIcon, s.mt2]}>
|
||||
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||
</View>
|
||||
<Text style={[s.white, s.pl5, s.flex1]}>
|
||||
This service has not provided terms of service or a privacy policy.
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
const els = []
|
||||
if (tos) {
|
||||
els.push(
|
||||
<TextLink
|
||||
key="tos"
|
||||
href={tos}
|
||||
text="Terms of Service"
|
||||
style={[s.white, s.underline]}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
if (pp) {
|
||||
els.push(
|
||||
<TextLink
|
||||
key="pp"
|
||||
href={pp}
|
||||
text="Privacy Policy"
|
||||
style={[s.white, s.underline]}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
if (els.length === 2) {
|
||||
els.splice(
|
||||
1,
|
||||
0,
|
||||
<Text key="and" style={s.white}>
|
||||
{' '}
|
||||
and{' '}
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
return (
|
||||
<View style={styles.policies}>
|
||||
<Text style={s.white}>
|
||||
By creating an account you agree to the {els}.
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const isReady = !!email && !!password && !!handle && is13
|
||||
return (
|
||||
<ScrollView testID="createAccount" style={{flex: 1}}>
|
||||
<KeyboardAvoidingView behavior="padding" style={{flex: 1}}>
|
||||
<View style={styles.logoHero}>
|
||||
<Logo />
|
||||
</View>
|
||||
<ScrollView testID="createAccount" style={pal.view}>
|
||||
<KeyboardAvoidingView behavior="padding">
|
||||
<LogoTextHero />
|
||||
{error ? (
|
||||
<View style={[styles.error, styles.errorFloating]}>
|
||||
<View style={styles.errorIcon}>
|
||||
<View style={[styles.errorIcon]}>
|
||||
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||
</View>
|
||||
<View style={s.flex1}>
|
||||
|
@ -189,41 +131,55 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
</View>
|
||||
</View>
|
||||
) : undefined}
|
||||
<View style={[styles.group]}>
|
||||
<View style={styles.groupTitle}>
|
||||
<Text style={[s.white, s.f18, s.bold]}>Create a new account</Text>
|
||||
</View>
|
||||
<View style={styles.groupContent}>
|
||||
<FontAwesomeIcon icon="globe" style={styles.groupContentIcon} />
|
||||
<View style={styles.groupLabel}>
|
||||
<Text type="sm-bold" style={pal.text}>
|
||||
Service provider
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pal.borderDark, styles.group]}>
|
||||
<View
|
||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||
<FontAwesomeIcon
|
||||
icon="globe"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
testID="registerSelectServiceButton"
|
||||
style={styles.textBtn}
|
||||
onPress={onPressSelectService}>
|
||||
<Text style={styles.textBtnLabel}>
|
||||
<Text type="xl" style={[pal.text, styles.textBtnLabel]}>
|
||||
{toNiceDomain(serviceUrl)}
|
||||
</Text>
|
||||
<View style={styles.textBtnFakeInnerBtn}>
|
||||
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="pen"
|
||||
size={12}
|
||||
style={styles.textBtnFakeInnerBtnIcon}
|
||||
style={[pal.textLight, styles.textBtnFakeInnerBtnIcon]}
|
||||
/>
|
||||
<Text style={styles.textBtnFakeInnerBtnLabel}>Change</Text>
|
||||
<Text style={[pal.textLight]}>Change</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{serviceDescription ? (
|
||||
<>
|
||||
</View>
|
||||
{serviceDescription ? (
|
||||
<>
|
||||
<View style={styles.groupLabel}>
|
||||
<Text type="sm-bold" style={pal.text}>
|
||||
Account details
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pal.borderDark, styles.group]}>
|
||||
{serviceDescription?.inviteCodeRequired ? (
|
||||
<View style={styles.groupContent}>
|
||||
<View
|
||||
style={[pal.border, styles.groupContent, styles.noTopBorder]}>
|
||||
<FontAwesomeIcon
|
||||
icon="ticket"
|
||||
style={styles.groupContentIcon}
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.textInput]}
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder="Invite code"
|
||||
placeholderTextColor={colors.blue0}
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
autoFocus
|
||||
|
@ -233,16 +189,16 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
/>
|
||||
</View>
|
||||
) : undefined}
|
||||
<View style={styles.groupContent}>
|
||||
<View style={[pal.border, styles.groupContent]}>
|
||||
<FontAwesomeIcon
|
||||
icon="envelope"
|
||||
style={styles.groupContentIcon}
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TextInput
|
||||
testID="registerEmailInput"
|
||||
style={[styles.textInput]}
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder="Email address"
|
||||
placeholderTextColor={colors.blue0}
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
value={email}
|
||||
|
@ -250,13 +206,16 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
editable={!isProcessing}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.groupContent}>
|
||||
<FontAwesomeIcon icon="lock" style={styles.groupContentIcon} />
|
||||
<View style={[pal.border, styles.groupContent]}>
|
||||
<FontAwesomeIcon
|
||||
icon="lock"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TextInput
|
||||
testID="registerPasswordInput"
|
||||
style={[styles.textInput]}
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder="Choose your password"
|
||||
placeholderTextColor={colors.blue0}
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
secureTextEntry
|
||||
|
@ -265,24 +224,28 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
editable={!isProcessing}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
) : undefined}
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
) : undefined}
|
||||
{serviceDescription ? (
|
||||
<>
|
||||
<View style={styles.group}>
|
||||
<View style={styles.groupTitle}>
|
||||
<Text style={[s.white, s.f18, s.bold]}>
|
||||
Choose your username
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.groupContent}>
|
||||
<FontAwesomeIcon icon="at" style={styles.groupContentIcon} />
|
||||
<View style={styles.groupLabel}>
|
||||
<Text type="sm-bold" style={pal.text}>
|
||||
Choose your username
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pal.border, styles.group]}>
|
||||
<View
|
||||
style={[pal.border, styles.groupContent, styles.noTopBorder]}>
|
||||
<FontAwesomeIcon
|
||||
icon="at"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TextInput
|
||||
testID="registerHandleInput"
|
||||
style={[styles.textInput]}
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder="eg alice"
|
||||
placeholderTextColor={colors.blue0}
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
value={handle}
|
||||
onChangeText={v => setHandle(makeValidHandle(v))}
|
||||
|
@ -290,15 +253,15 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
/>
|
||||
</View>
|
||||
{serviceDescription.availableUserDomains.length > 1 && (
|
||||
<View style={styles.groupContent}>
|
||||
<View style={[pal.border, styles.groupContent]}>
|
||||
<FontAwesomeIcon
|
||||
icon="globe"
|
||||
style={styles.groupContentIcon}
|
||||
/>
|
||||
<Picker
|
||||
style={styles.picker}
|
||||
style={[pal.text, styles.picker]}
|
||||
labelStyle={styles.pickerLabel}
|
||||
iconStyle={styles.pickerIcon}
|
||||
iconStyle={pal.textLight}
|
||||
value={userDomain}
|
||||
items={serviceDescription.availableUserDomains.map(d => ({
|
||||
label: `.${d}`,
|
||||
|
@ -309,41 +272,50 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
/>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.groupContent}>
|
||||
<Text style={[s.white, s.p10]}>
|
||||
<View style={[pal.border, styles.groupContent]}>
|
||||
<Text style={[pal.textLight, s.p10]}>
|
||||
Your full username will be{' '}
|
||||
<Text style={[s.white, s.bold]}>
|
||||
<Text type="md-bold" style={pal.textLight}>
|
||||
@{createFullHandle(handle, userDomain)}
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.group]}>
|
||||
<View style={styles.groupTitle}>
|
||||
<Text style={[s.white, s.f18, s.bold]}>Legal</Text>
|
||||
</View>
|
||||
<View style={styles.groupContent}>
|
||||
<View style={styles.groupLabel}>
|
||||
<Text type="sm-bold" style={pal.text}>
|
||||
Legal
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pal.border, styles.group]}>
|
||||
<View
|
||||
style={[pal.border, styles.groupContent, styles.noTopBorder]}>
|
||||
<TouchableOpacity
|
||||
testID="registerIs13Input"
|
||||
style={styles.textBtn}
|
||||
onPress={() => setIs13(!is13)}>
|
||||
<View style={is13 ? styles.checkboxFilled : styles.checkbox}>
|
||||
<View
|
||||
style={[
|
||||
pal.border,
|
||||
is13 ? styles.checkboxFilled : styles.checkbox,
|
||||
]}>
|
||||
{is13 && (
|
||||
<FontAwesomeIcon icon="check" style={s.blue3} size={14} />
|
||||
)}
|
||||
</View>
|
||||
<Text style={[styles.textBtnLabel, s.f16]}>
|
||||
<Text style={[pal.text, styles.textBtnLabel]}>
|
||||
I am 13 years old or older
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<Policies />
|
||||
<Policies serviceDescription={serviceDescription} />
|
||||
</>
|
||||
) : undefined}
|
||||
<View style={[s.flexRow, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack}>
|
||||
<Text style={[s.white, s.f18, s.pl5]}>Back</Text>
|
||||
<Text type="xl" style={pal.link}>
|
||||
Back
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
{isReady ? (
|
||||
|
@ -351,21 +323,27 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
testID="createAccountButton"
|
||||
onPress={onPressNext}>
|
||||
{isProcessing ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text>
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Next
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : !serviceDescription && error ? (
|
||||
<TouchableOpacity
|
||||
testID="registerRetryButton"
|
||||
onPress={onPressRetryConnect}>
|
||||
<Text style={[s.white, s.f18, s.bold, s.pr5]}>Retry</Text>
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Retry
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : !serviceDescription ? (
|
||||
<>
|
||||
<ActivityIndicator color="#fff" />
|
||||
<Text style={[s.white, s.f18, s.pl5, s.pr5]}>Connecting...</Text>
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Connecting...
|
||||
</Text>
|
||||
</>
|
||||
) : undefined}
|
||||
</View>
|
||||
|
@ -375,6 +353,69 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
)
|
||||
}
|
||||
|
||||
const Policies = ({
|
||||
serviceDescription,
|
||||
}: {
|
||||
serviceDescription: ServiceDescription
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
if (!serviceDescription) {
|
||||
return <View />
|
||||
}
|
||||
const tos = validWebLink(serviceDescription.links?.termsOfService)
|
||||
const pp = validWebLink(serviceDescription.links?.privacyPolicy)
|
||||
if (!tos && !pp) {
|
||||
return (
|
||||
<View style={styles.policies}>
|
||||
<View style={[styles.errorIcon, {borderColor: pal.colors.text}, s.mt2]}>
|
||||
<FontAwesomeIcon icon="exclamation" style={pal.textLight} size={10} />
|
||||
</View>
|
||||
<Text style={[pal.textLight, s.pl5, s.flex1]}>
|
||||
This service has not provided terms of service or a privacy policy.
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
const els = []
|
||||
if (tos) {
|
||||
els.push(
|
||||
<TextLink
|
||||
key="tos"
|
||||
href={tos}
|
||||
text="Terms of Service"
|
||||
style={[pal.link, s.underline]}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
if (pp) {
|
||||
els.push(
|
||||
<TextLink
|
||||
key="pp"
|
||||
href={pp}
|
||||
text="Privacy Policy"
|
||||
style={[pal.link, s.underline]}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
if (els.length === 2) {
|
||||
els.splice(
|
||||
1,
|
||||
0,
|
||||
<Text key="and" style={pal.textLight}>
|
||||
{' '}
|
||||
and{' '}
|
||||
</Text>,
|
||||
)
|
||||
}
|
||||
return (
|
||||
<View style={styles.policies}>
|
||||
<Text style={pal.textLight}>
|
||||
By creating an account you agree to the {els}.
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function validWebLink(url?: string): string | undefined {
|
||||
return url && (url.startsWith('http://') || url.startsWith('https://'))
|
||||
? url
|
||||
|
@ -382,42 +423,39 @@ function validWebLink(url?: string): string | undefined {
|
|||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
noTopBorder: {
|
||||
borderTopWidth: 0,
|
||||
},
|
||||
logoHero: {
|
||||
paddingTop: 30,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
group: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.white,
|
||||
borderRadius: 10,
|
||||
marginBottom: 20,
|
||||
marginHorizontal: 20,
|
||||
backgroundColor: colors.blue3,
|
||||
},
|
||||
groupTitle: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
groupLabel: {
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 5,
|
||||
},
|
||||
groupContent: {
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.blue1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
groupContentIcon: {
|
||||
color: 'white',
|
||||
marginLeft: 10,
|
||||
},
|
||||
textInput: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
backgroundColor: colors.blue3,
|
||||
color: colors.white,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
fontSize: 18,
|
||||
fontSize: 17,
|
||||
letterSpacing: 0.25,
|
||||
fontWeight: '400',
|
||||
borderRadius: 10,
|
||||
},
|
||||
textBtn: {
|
||||
|
@ -427,47 +465,33 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
textBtnLabel: {
|
||||
flex: 1,
|
||||
color: colors.white,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
fontSize: 18,
|
||||
},
|
||||
textBtnFakeInnerBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.blue2,
|
||||
borderRadius: 6,
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 8,
|
||||
marginHorizontal: 6,
|
||||
},
|
||||
textBtnFakeInnerBtnIcon: {
|
||||
color: colors.white,
|
||||
marginRight: 4,
|
||||
},
|
||||
textBtnFakeInnerBtnLabel: {
|
||||
color: colors.white,
|
||||
},
|
||||
picker: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
backgroundColor: colors.blue3,
|
||||
color: colors.white,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
fontSize: 18,
|
||||
fontSize: 17,
|
||||
borderRadius: 10,
|
||||
},
|
||||
pickerLabel: {
|
||||
color: colors.white,
|
||||
fontSize: 18,
|
||||
},
|
||||
pickerIcon: {
|
||||
color: colors.white,
|
||||
fontSize: 17,
|
||||
},
|
||||
checkbox: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.white,
|
||||
borderRadius: 2,
|
||||
width: 16,
|
||||
height: 16,
|
||||
|
@ -475,8 +499,6 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
checkboxFilled: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.white,
|
||||
backgroundColor: colors.white,
|
||||
borderRadius: 2,
|
||||
width: 16,
|
||||
height: 16,
|
||||
|
@ -489,8 +511,6 @@ const styles = StyleSheet.create({
|
|||
paddingBottom: 20,
|
||||
},
|
||||
error: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.red5,
|
||||
backgroundColor: colors.red4,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
@ -509,7 +529,6 @@ const styles = StyleSheet.create({
|
|||
errorIcon: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.white,
|
||||
color: colors.white,
|
||||
borderRadius: 30,
|
||||
width: 16,
|
||||
height: 16,
|
||||
|
|
|
@ -1,26 +1,29 @@
|
|||
import React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import Svg, {Circle, Line, Text as SvgText} from 'react-native-svg'
|
||||
import {s, gradients} from '../../lib/styles'
|
||||
import {Text} from '../util/text/Text'
|
||||
|
||||
export const Logo = () => {
|
||||
export const Logo = ({color, size = 100}: {color: string; size?: number}) => {
|
||||
return (
|
||||
<View style={styles.logo}>
|
||||
<Svg width="100" height="100">
|
||||
<Svg width={size} height={size} viewBox="0 0 100 100">
|
||||
<Circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="46"
|
||||
fill="none"
|
||||
stroke="white"
|
||||
stroke={color}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<Line stroke="white" strokeWidth={1} x1="30" x2="30" y1="0" y2="100" />
|
||||
<Line stroke="white" strokeWidth={1} x1="74" x2="74" y1="0" y2="100" />
|
||||
<Line stroke="white" strokeWidth={1} x1="0" x2="100" y1="22" y2="22" />
|
||||
<Line stroke="white" strokeWidth={1} x1="0" x2="100" y1="74" y2="74" />
|
||||
<Line stroke={color} strokeWidth={1} x1="30" x2="30" y1="0" y2="100" />
|
||||
<Line stroke={color} strokeWidth={1} x1="74" x2="74" y1="0" y2="100" />
|
||||
<Line stroke={color} strokeWidth={1} x1="0" x2="100" y1="22" y2="22" />
|
||||
<Line stroke={color} strokeWidth={1} x1="0" x2="100" y1="74" y2="74" />
|
||||
<SvgText
|
||||
fill="none"
|
||||
stroke="white"
|
||||
stroke={color}
|
||||
strokeWidth={2}
|
||||
fontSize="60"
|
||||
fontWeight="bold"
|
||||
|
@ -34,9 +37,32 @@ export const Logo = () => {
|
|||
)
|
||||
}
|
||||
|
||||
export const LogoTextHero = () => {
|
||||
return (
|
||||
<LinearGradient
|
||||
colors={[gradients.blue.start, gradients.blue.end]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[styles.textHero]}>
|
||||
<Logo color="white" size={40} />
|
||||
<Text type="title-lg" style={[s.white, s.pl10]}>
|
||||
Bluesky
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
logo: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
textHero: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingRight: 20,
|
||||
paddingVertical: 15,
|
||||
marginBottom: 20,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -11,23 +11,28 @@ import {
|
|||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import * as EmailValidator from 'email-validator'
|
||||
import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
|
||||
import {Logo} from './Logo'
|
||||
import {LogoTextHero} from './Logo'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import {s, colors} from '../../lib/styles'
|
||||
import {createFullHandle, toNiceDomain} from '../../../lib/strings'
|
||||
import {useStores, RootStoreModel, DEFAULT_SERVICE} from '../../../state'
|
||||
import {ServiceDescription} from '../../../state/models/session'
|
||||
import {ServerInputModal} from '../../../state/models/shell-ui'
|
||||
import {AccountData} from '../../../state/models/session'
|
||||
import {isNetworkError} from '../../../lib/errors'
|
||||
import {usePalette} from '../../lib/hooks/usePalette'
|
||||
|
||||
enum Forms {
|
||||
Login,
|
||||
ChooseAccount,
|
||||
ForgotPassword,
|
||||
SetNewPassword,
|
||||
PasswordUpdated,
|
||||
}
|
||||
|
||||
export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const [error, setError] = useState<string>('')
|
||||
const [retryDescribeTrigger, setRetryDescribeTrigger] = useState<any>({})
|
||||
|
@ -35,7 +40,18 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
const [serviceDescription, setServiceDescription] = useState<
|
||||
ServiceDescription | undefined
|
||||
>(undefined)
|
||||
const [currentForm, setCurrentForm] = useState<Forms>(Forms.Login)
|
||||
const [initialHandle, setInitialHandle] = useState<string>('')
|
||||
const [currentForm, setCurrentForm] = useState<Forms>(
|
||||
store.session.hasAccounts ? Forms.ChooseAccount : Forms.Login,
|
||||
)
|
||||
|
||||
const onSelectAccount = (account?: AccountData) => {
|
||||
if (account?.service) {
|
||||
setServiceUrl(account.service)
|
||||
}
|
||||
setInitialHandle(account?.handle || '')
|
||||
setCurrentForm(Forms.Login)
|
||||
}
|
||||
|
||||
const gotoForm = (form: Forms) => () => {
|
||||
setError('')
|
||||
|
@ -73,16 +89,14 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
const onPressRetryConnect = () => setRetryDescribeTrigger({})
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView testID="signIn" behavior="padding" style={{flex: 1}}>
|
||||
<View style={styles.logoHero}>
|
||||
<Logo />
|
||||
</View>
|
||||
<KeyboardAvoidingView testID="signIn" behavior="padding" style={[pal.view]}>
|
||||
{currentForm === Forms.Login ? (
|
||||
<LoginForm
|
||||
store={store}
|
||||
error={error}
|
||||
serviceUrl={serviceUrl}
|
||||
serviceDescription={serviceDescription}
|
||||
initialHandle={initialHandle}
|
||||
setError={setError}
|
||||
setServiceUrl={setServiceUrl}
|
||||
onPressBack={onPressBack}
|
||||
|
@ -90,6 +104,13 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
onPressRetryConnect={onPressRetryConnect}
|
||||
/>
|
||||
) : undefined}
|
||||
{currentForm === Forms.ChooseAccount ? (
|
||||
<ChooseAccountForm
|
||||
store={store}
|
||||
onSelectAccount={onSelectAccount}
|
||||
onPressBack={onPressBack}
|
||||
/>
|
||||
) : undefined}
|
||||
{currentForm === Forms.ForgotPassword ? (
|
||||
<ForgotPasswordForm
|
||||
store={store}
|
||||
|
@ -119,11 +140,109 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
|
|||
)
|
||||
}
|
||||
|
||||
const ChooseAccountForm = ({
|
||||
store,
|
||||
onSelectAccount,
|
||||
onPressBack,
|
||||
}: {
|
||||
store: RootStoreModel
|
||||
onSelectAccount: (account?: AccountData) => void
|
||||
onPressBack: () => void
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const [isProcessing, setIsProcessing] = React.useState(false)
|
||||
|
||||
const onTryAccount = async (account: AccountData) => {
|
||||
if (account.accessJwt && account.refreshJwt) {
|
||||
setIsProcessing(true)
|
||||
if (await store.session.resumeSession(account)) {
|
||||
setIsProcessing(false)
|
||||
return
|
||||
}
|
||||
setIsProcessing(false)
|
||||
}
|
||||
onSelectAccount(account)
|
||||
}
|
||||
|
||||
return (
|
||||
<View testID="chooseAccountForm">
|
||||
<LogoTextHero />
|
||||
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
|
||||
Sign in as...
|
||||
</Text>
|
||||
{store.session.accounts.map(account => (
|
||||
<TouchableOpacity
|
||||
testID={`chooseAccountBtn-${account.handle}`}
|
||||
key={account.did}
|
||||
style={[pal.borderDark, styles.group, s.mb5]}
|
||||
onPress={() => onTryAccount(account)}>
|
||||
<View
|
||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||
<View style={s.p10}>
|
||||
<UserAvatar
|
||||
displayName={account.displayName}
|
||||
handle={account.handle}
|
||||
avatar={account.aviUrl}
|
||||
size={30}
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.accountText}>
|
||||
<Text type="lg-bold" style={pal.text}>
|
||||
{account.displayName || account.handle}{' '}
|
||||
</Text>
|
||||
<Text type="lg" style={[pal.textLight]}>
|
||||
{account.handle}
|
||||
</Text>
|
||||
</Text>
|
||||
<FontAwesomeIcon
|
||||
icon="angle-right"
|
||||
size={16}
|
||||
style={[pal.text, s.mr10]}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
<TouchableOpacity
|
||||
testID="chooseNewAccountBtn"
|
||||
style={[pal.borderDark, styles.group]}
|
||||
onPress={() => onSelectAccount(undefined)}>
|
||||
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||
<View style={s.p10}>
|
||||
<View
|
||||
style={[pal.btn, {width: 30, height: 30, borderRadius: 15}]}
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.accountText}>
|
||||
<Text type="lg" style={pal.text}>
|
||||
Other account
|
||||
</Text>
|
||||
</Text>
|
||||
<FontAwesomeIcon
|
||||
icon="angle-right"
|
||||
size={16}
|
||||
style={[pal.text, s.mr10]}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack}>
|
||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||
Back
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
{isProcessing && <ActivityIndicator />}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const LoginForm = ({
|
||||
store,
|
||||
error,
|
||||
serviceUrl,
|
||||
serviceDescription,
|
||||
initialHandle,
|
||||
setError,
|
||||
setServiceUrl,
|
||||
onPressRetryConnect,
|
||||
|
@ -134,14 +253,16 @@ const LoginForm = ({
|
|||
error: string
|
||||
serviceUrl: string
|
||||
serviceDescription: ServiceDescription | undefined
|
||||
initialHandle: string
|
||||
setError: (v: string) => void
|
||||
setServiceUrl: (v: string) => void
|
||||
onPressRetryConnect: () => void
|
||||
onPressBack: () => void
|
||||
onPressForgotPassword: () => void
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||
const [handle, setHandle] = useState<string>('')
|
||||
const [handle, setHandle] = useState<string>(initialHandle)
|
||||
const [password, setPassword] = useState<string>('')
|
||||
|
||||
const onPressSelectService = () => {
|
||||
|
@ -197,31 +318,44 @@ const LoginForm = ({
|
|||
|
||||
const isReady = !!serviceDescription && !!handle && !!password
|
||||
return (
|
||||
<>
|
||||
<View testID="loginFormView" style={styles.group}>
|
||||
<TouchableOpacity
|
||||
testID="loginSelectServiceButton"
|
||||
style={[styles.groupTitle, {paddingRight: 0, paddingVertical: 6}]}
|
||||
onPress={onPressSelectService}>
|
||||
<Text style={[s.flex1, s.white, s.f18, s.bold]} numberOfLines={1}>
|
||||
Sign in to {toNiceDomain(serviceUrl)}
|
||||
</Text>
|
||||
<View style={styles.textBtnFakeInnerBtn}>
|
||||
<FontAwesomeIcon
|
||||
icon="pen"
|
||||
size={12}
|
||||
style={styles.textBtnFakeInnerBtnIcon}
|
||||
/>
|
||||
<Text style={styles.textBtnFakeInnerBtnLabel}>Change</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.groupContent}>
|
||||
<FontAwesomeIcon icon="at" style={styles.groupContentIcon} />
|
||||
<View testID="loginForm">
|
||||
<LogoTextHero />
|
||||
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
|
||||
Sign into
|
||||
</Text>
|
||||
<View style={[pal.borderDark, styles.group]}>
|
||||
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||
<FontAwesomeIcon
|
||||
icon="globe"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
testID="loginSelectServiceButton"
|
||||
style={styles.textBtn}
|
||||
onPress={onPressSelectService}>
|
||||
<Text type="xl" style={[pal.text, styles.textBtnLabel]}>
|
||||
{toNiceDomain(serviceUrl)}
|
||||
</Text>
|
||||
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
|
||||
<FontAwesomeIcon icon="pen" size={12} style={pal.textLight} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
|
||||
Account
|
||||
</Text>
|
||||
<View style={[pal.borderDark, styles.group]}>
|
||||
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||
<FontAwesomeIcon
|
||||
icon="at"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TextInput
|
||||
testID="loginUsernameInput"
|
||||
style={styles.textInput}
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder="Username"
|
||||
placeholderTextColor={colors.blue0}
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
autoFocus
|
||||
autoCorrect={false}
|
||||
|
@ -230,13 +364,16 @@ const LoginForm = ({
|
|||
editable={!isProcessing}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.groupContent}>
|
||||
<FontAwesomeIcon icon="lock" style={styles.groupContentIcon} />
|
||||
<View style={[pal.borderDark, styles.groupContent]}>
|
||||
<FontAwesomeIcon
|
||||
icon="lock"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TextInput
|
||||
testID="loginPasswordInput"
|
||||
style={styles.textInput}
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder="Password"
|
||||
placeholderTextColor={colors.blue0}
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
secureTextEntry
|
||||
|
@ -248,7 +385,7 @@ const LoginForm = ({
|
|||
testID="forgotPasswordButton"
|
||||
style={styles.textInputInnerBtn}
|
||||
onPress={onPressForgotPassword}>
|
||||
<Text style={styles.textInputInnerBtnLabel}>Forgot</Text>
|
||||
<Text style={pal.link}>Forgot</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
@ -264,29 +401,37 @@ const LoginForm = ({
|
|||
) : undefined}
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack}>
|
||||
<Text style={[s.white, s.f18, s.pl5]}>Back</Text>
|
||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||
Back
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
{!serviceDescription && error ? (
|
||||
<TouchableOpacity
|
||||
testID="loginRetryButton"
|
||||
onPress={onPressRetryConnect}>
|
||||
<Text style={[s.white, s.f18, s.bold, s.pr5]}>Retry</Text>
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Retry
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : !serviceDescription ? (
|
||||
<>
|
||||
<ActivityIndicator color="#fff" />
|
||||
<Text style={[s.white, s.f18, s.pl10]}>Connecting...</Text>
|
||||
<ActivityIndicator />
|
||||
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
||||
Connecting...
|
||||
</Text>
|
||||
</>
|
||||
) : isProcessing ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
<ActivityIndicator />
|
||||
) : isReady ? (
|
||||
<TouchableOpacity testID="loginNextButton" onPress={onPressNext}>
|
||||
<Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text>
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Next
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : undefined}
|
||||
</View>
|
||||
</>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -309,6 +454,7 @@ const ForgotPasswordForm = ({
|
|||
onPressBack: () => void
|
||||
onEmailSent: () => void
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||
const [email, setEmail] = useState<string>('')
|
||||
|
||||
|
@ -344,72 +490,88 @@ const ForgotPasswordForm = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Text style={styles.screenTitle}>Reset password</Text>
|
||||
<Text style={styles.instructions}>
|
||||
Enter the email you used to create your account. We'll send you a "reset
|
||||
code" so you can set a new password.
|
||||
</Text>
|
||||
<View testID="forgotPasswordView" style={styles.group}>
|
||||
<TouchableOpacity
|
||||
testID="forgotPasswordSelectServiceButton"
|
||||
style={[styles.groupContent, {borderTopWidth: 0}]}
|
||||
onPress={onPressSelectService}>
|
||||
<FontAwesomeIcon icon="globe" style={styles.groupContentIcon} />
|
||||
<Text style={styles.textInput} numberOfLines={1}>
|
||||
{toNiceDomain(serviceUrl)}
|
||||
</Text>
|
||||
<View style={styles.textBtnFakeInnerBtn}>
|
||||
<LogoTextHero />
|
||||
<View>
|
||||
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
||||
Reset password
|
||||
</Text>
|
||||
<Text type="md" style={[pal.text, styles.instructions]}>
|
||||
Enter the email you used to create your account. We'll send you a
|
||||
"reset code" so you can set a new password.
|
||||
</Text>
|
||||
<View
|
||||
testID="forgotPasswordView"
|
||||
style={[pal.borderDark, pal.view, styles.group]}>
|
||||
<TouchableOpacity
|
||||
testID="forgotPasswordSelectServiceButton"
|
||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}
|
||||
onPress={onPressSelectService}>
|
||||
<FontAwesomeIcon
|
||||
icon="pen"
|
||||
size={12}
|
||||
style={styles.textBtnFakeInnerBtnIcon}
|
||||
icon="globe"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<Text style={styles.textBtnFakeInnerBtnLabel}>Change</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.groupContent}>
|
||||
<FontAwesomeIcon icon="envelope" style={styles.groupContentIcon} />
|
||||
<TextInput
|
||||
testID="forgotPasswordEmail"
|
||||
style={styles.textInput}
|
||||
placeholder="Email address"
|
||||
placeholderTextColor={colors.blue0}
|
||||
autoCapitalize="none"
|
||||
autoFocus
|
||||
autoCorrect={false}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
editable={!isProcessing}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{error ? (
|
||||
<View style={styles.error}>
|
||||
<View style={styles.errorIcon}>
|
||||
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||
</View>
|
||||
<View style={s.flex1}>
|
||||
<Text style={[s.white, s.bold]}>{error}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : undefined}
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack}>
|
||||
<Text style={[s.white, s.f18, s.pl5]}>Back</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
{!serviceDescription || isProcessing ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : !email ? (
|
||||
<Text style={[s.blue1, s.f18, s.bold, s.pr5]}>Next</Text>
|
||||
) : (
|
||||
<TouchableOpacity testID="newPasswordButton" onPress={onPressNext}>
|
||||
<Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text>
|
||||
<Text style={[pal.text, styles.textInput]} numberOfLines={1}>
|
||||
{toNiceDomain(serviceUrl)}
|
||||
</Text>
|
||||
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
|
||||
<FontAwesomeIcon icon="pen" size={12} style={pal.text} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{!serviceDescription || isProcessing ? (
|
||||
<Text style={[s.white, s.f18, s.pl10]}>Processing...</Text>
|
||||
<View style={[pal.borderDark, styles.groupContent]}>
|
||||
<FontAwesomeIcon
|
||||
icon="envelope"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TextInput
|
||||
testID="forgotPasswordEmail"
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder="Email address"
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
autoFocus
|
||||
autoCorrect={false}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
editable={!isProcessing}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{error ? (
|
||||
<View style={styles.error}>
|
||||
<View style={styles.errorIcon}>
|
||||
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||
</View>
|
||||
<View style={s.flex1}>
|
||||
<Text style={[s.white, s.bold]}>{error}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : undefined}
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack}>
|
||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||
Back
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
{!serviceDescription || isProcessing ? (
|
||||
<ActivityIndicator />
|
||||
) : !email ? (
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5, {opacity: 0.5}]}>
|
||||
Next
|
||||
</Text>
|
||||
) : (
|
||||
<TouchableOpacity testID="newPasswordButton" onPress={onPressNext}>
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Next
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{!serviceDescription || isProcessing ? (
|
||||
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
||||
Processing...
|
||||
</Text>
|
||||
) : undefined}
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
|
@ -430,6 +592,7 @@ const SetNewPasswordForm = ({
|
|||
onPressBack: () => void
|
||||
onPasswordSet: () => void
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||
const [resetCode, setResetCode] = useState<string>('')
|
||||
const [password, setPassword] = useState<string>('')
|
||||
|
@ -458,87 +621,119 @@ const SetNewPasswordForm = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Text style={styles.screenTitle}>Set new password</Text>
|
||||
<Text style={styles.instructions}>
|
||||
You will receive an email with a "reset code." Enter that code here,
|
||||
then enter your new password.
|
||||
</Text>
|
||||
<View testID="newPasswordView" style={styles.group}>
|
||||
<View style={[styles.groupContent, {borderTopWidth: 0}]}>
|
||||
<FontAwesomeIcon icon="ticket" style={styles.groupContentIcon} />
|
||||
<TextInput
|
||||
testID="resetCodeInput"
|
||||
style={[styles.textInput]}
|
||||
placeholder="Reset code"
|
||||
placeholderTextColor={colors.blue0}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
autoFocus
|
||||
value={resetCode}
|
||||
onChangeText={setResetCode}
|
||||
editable={!isProcessing}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.groupContent}>
|
||||
<FontAwesomeIcon icon="lock" style={styles.groupContentIcon} />
|
||||
<TextInput
|
||||
testID="newPasswordInput"
|
||||
style={styles.textInput}
|
||||
placeholder="New password"
|
||||
placeholderTextColor={colors.blue0}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
editable={!isProcessing}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{error ? (
|
||||
<View style={styles.error}>
|
||||
<View style={styles.errorIcon}>
|
||||
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||
<LogoTextHero />
|
||||
<View>
|
||||
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
||||
Set new password
|
||||
</Text>
|
||||
<Text type="lg" style={[pal.text, styles.instructions]}>
|
||||
You will receive an email with a "reset code." Enter that code here,
|
||||
then enter your new password.
|
||||
</Text>
|
||||
<View
|
||||
testID="newPasswordView"
|
||||
style={[pal.view, pal.borderDark, styles.group]}>
|
||||
<View
|
||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||
<FontAwesomeIcon
|
||||
icon="ticket"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TextInput
|
||||
testID="resetCodeInput"
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder="Reset code"
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
autoFocus
|
||||
value={resetCode}
|
||||
onChangeText={setResetCode}
|
||||
editable={!isProcessing}
|
||||
/>
|
||||
</View>
|
||||
<View style={s.flex1}>
|
||||
<Text style={[s.white, s.bold]}>{error}</Text>
|
||||
<View style={[pal.borderDark, styles.groupContent]}>
|
||||
<FontAwesomeIcon
|
||||
icon="lock"
|
||||
style={[pal.textLight, styles.groupContentIcon]}
|
||||
/>
|
||||
<TextInput
|
||||
testID="newPasswordInput"
|
||||
style={[pal.text, styles.textInput]}
|
||||
placeholder="New password"
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
secureTextEntry
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
editable={!isProcessing}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
) : undefined}
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack}>
|
||||
<Text style={[s.white, s.f18, s.pl5]}>Back</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
{isProcessing ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : !resetCode || !password ? (
|
||||
<Text style={[s.blue1, s.f18, s.bold, s.pr5]}>Next</Text>
|
||||
) : (
|
||||
<TouchableOpacity testID="setNewPasswordButton" onPress={onPressNext}>
|
||||
<Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{isProcessing ? (
|
||||
<Text style={[s.white, s.f18, s.pl10]}>Updating...</Text>
|
||||
{error ? (
|
||||
<View style={styles.error}>
|
||||
<View style={styles.errorIcon}>
|
||||
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
||||
</View>
|
||||
<View style={s.flex1}>
|
||||
<Text style={[s.white, s.bold]}>{error}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : undefined}
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<TouchableOpacity onPress={onPressBack}>
|
||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
||||
Back
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
{isProcessing ? (
|
||||
<ActivityIndicator />
|
||||
) : !resetCode || !password ? (
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5, {opacity: 0.5}]}>
|
||||
Next
|
||||
</Text>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
testID="setNewPasswordButton"
|
||||
onPress={onPressNext}>
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Next
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{isProcessing ? (
|
||||
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
||||
Updating...
|
||||
</Text>
|
||||
) : undefined}
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const PasswordUpdatedForm = ({onPressNext}: {onPressNext: () => void}) => {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<>
|
||||
<Text style={styles.screenTitle}>Password updated!</Text>
|
||||
<Text style={styles.instructions}>
|
||||
You can now sign in with your new password.
|
||||
</Text>
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<View style={s.flex1} />
|
||||
<TouchableOpacity onPress={onPressNext}>
|
||||
<Text style={[s.white, s.f18, s.bold, s.pr5]}>Okay</Text>
|
||||
</TouchableOpacity>
|
||||
<LogoTextHero />
|
||||
<View>
|
||||
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
||||
Password updated!
|
||||
</Text>
|
||||
<Text type="lg" style={[pal.text, styles.instructions]}>
|
||||
You can now sign in with your new password.
|
||||
</Text>
|
||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
||||
<View style={s.flex1} />
|
||||
<TouchableOpacity onPress={onPressNext}>
|
||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||
Okay
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
|
@ -546,53 +741,42 @@ const PasswordUpdatedForm = ({onPressNext}: {onPressNext: () => void}) => {
|
|||
|
||||
const styles = StyleSheet.create({
|
||||
screenTitle: {
|
||||
color: colors.white,
|
||||
fontSize: 26,
|
||||
marginBottom: 10,
|
||||
marginHorizontal: 20,
|
||||
},
|
||||
instructions: {
|
||||
color: colors.white,
|
||||
fontSize: 16,
|
||||
marginBottom: 20,
|
||||
marginHorizontal: 20,
|
||||
},
|
||||
logoHero: {
|
||||
paddingTop: 30,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
group: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.white,
|
||||
borderRadius: 10,
|
||||
marginBottom: 20,
|
||||
marginHorizontal: 20,
|
||||
backgroundColor: colors.blue3,
|
||||
},
|
||||
groupTitle: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
groupLabel: {
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 5,
|
||||
},
|
||||
groupContent: {
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.blue1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
noTopBorder: {
|
||||
borderTopWidth: 0,
|
||||
},
|
||||
groupContentIcon: {
|
||||
color: 'white',
|
||||
marginLeft: 10,
|
||||
},
|
||||
textInput: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
backgroundColor: colors.blue3,
|
||||
color: colors.white,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
fontSize: 18,
|
||||
fontSize: 17,
|
||||
letterSpacing: 0.25,
|
||||
fontWeight: '400',
|
||||
borderRadius: 10,
|
||||
},
|
||||
textInputInnerBtn: {
|
||||
|
@ -602,28 +786,31 @@ const styles = StyleSheet.create({
|
|||
paddingHorizontal: 8,
|
||||
marginHorizontal: 6,
|
||||
},
|
||||
textInputInnerBtnLabel: {
|
||||
color: colors.white,
|
||||
textBtn: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
},
|
||||
textBtnLabel: {
|
||||
flex: 1,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
textBtnFakeInnerBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.blue2,
|
||||
borderRadius: 6,
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 8,
|
||||
marginHorizontal: 6,
|
||||
},
|
||||
textBtnFakeInnerBtnIcon: {
|
||||
color: colors.white,
|
||||
marginRight: 4,
|
||||
},
|
||||
textBtnFakeInnerBtnLabel: {
|
||||
color: colors.white,
|
||||
accountText: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
error: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.red5,
|
||||
backgroundColor: colors.red4,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
|
|
@ -33,7 +33,7 @@ export function Component({
|
|||
}
|
||||
|
||||
return (
|
||||
<View style={s.flex1}>
|
||||
<View style={s.flex1} testID="serverInputModal">
|
||||
<Text style={[s.textCenter, s.bold, s.f18]}>Choose Service</Text>
|
||||
<BottomSheetScrollView style={styles.inner}>
|
||||
<View style={styles.group}>
|
||||
|
@ -64,6 +64,7 @@ export function Component({
|
|||
<Text style={styles.label}>Other service</Text>
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<BottomSheetTextInput
|
||||
testID="customServerTextInput"
|
||||
style={styles.textInput}
|
||||
placeholder="e.g. https://bsky.app"
|
||||
placeholderTextColor={colors.gray4}
|
||||
|
@ -74,6 +75,7 @@ export function Component({
|
|||
onChangeText={setCustomUrl}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
testID="customServerSelectBtn"
|
||||
style={styles.textInputBtn}
|
||||
onPress={() => doSelect(customUrl)}>
|
||||
<FontAwesomeIcon
|
||||
|
|
|
@ -49,6 +49,7 @@ export const ViewHeader = observer(function ViewHeader({
|
|||
return (
|
||||
<View style={[styles.header, pal.view]}>
|
||||
<TouchableOpacity
|
||||
testID="viewHeaderBackOrMenuBtn"
|
||||
onPress={canGoBack ? onPressBack : onPressMenu}
|
||||
hitSlop={BACK_HITSLOP}
|
||||
style={canGoBack ? styles.backIcon : styles.backIconWide}>
|
||||
|
|
|
@ -18,6 +18,7 @@ export type PaletteColor = {
|
|||
textInverted: string
|
||||
link: string
|
||||
border: string
|
||||
borderDark: string
|
||||
icon: string
|
||||
[k: string]: string
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ export interface UsePaletteValue {
|
|||
view: ViewStyle
|
||||
btn: ViewStyle
|
||||
border: ViewStyle
|
||||
borderDark: ViewStyle
|
||||
text: TextStyle
|
||||
textLight: TextStyle
|
||||
textInverted: TextStyle
|
||||
|
@ -25,6 +26,9 @@ export function usePalette(color: PaletteColorName): UsePaletteValue {
|
|||
border: {
|
||||
borderColor: palette.border,
|
||||
},
|
||||
borderDark: {
|
||||
borderColor: palette.borderDark,
|
||||
},
|
||||
text: {
|
||||
color: palette.text,
|
||||
},
|
||||
|
|
|
@ -13,6 +13,7 @@ export const defaultTheme: Theme = {
|
|||
textInverted: colors.white,
|
||||
link: colors.blue3,
|
||||
border: '#f0e9e9',
|
||||
borderDark: '#e0d9d9',
|
||||
icon: colors.gray3,
|
||||
|
||||
// non-standard
|
||||
|
@ -32,6 +33,7 @@ export const defaultTheme: Theme = {
|
|||
textInverted: colors.blue3,
|
||||
link: colors.blue0,
|
||||
border: colors.blue4,
|
||||
borderDark: colors.blue5,
|
||||
icon: colors.blue4,
|
||||
},
|
||||
secondary: {
|
||||
|
@ -42,6 +44,7 @@ export const defaultTheme: Theme = {
|
|||
textInverted: colors.green4,
|
||||
link: colors.green1,
|
||||
border: colors.green4,
|
||||
borderDark: colors.green5,
|
||||
icon: colors.green4,
|
||||
},
|
||||
inverted: {
|
||||
|
@ -52,6 +55,7 @@ export const defaultTheme: Theme = {
|
|||
textInverted: colors.black,
|
||||
link: colors.blue2,
|
||||
border: colors.gray3,
|
||||
borderDark: colors.gray2,
|
||||
icon: colors.gray5,
|
||||
},
|
||||
error: {
|
||||
|
@ -62,6 +66,7 @@ export const defaultTheme: Theme = {
|
|||
textInverted: colors.red3,
|
||||
link: colors.red1,
|
||||
border: colors.red4,
|
||||
borderDark: colors.red5,
|
||||
icon: colors.red4,
|
||||
},
|
||||
},
|
||||
|
@ -257,6 +262,7 @@ export const darkTheme: Theme = {
|
|||
textInverted: colors.black,
|
||||
link: colors.blue3,
|
||||
border: colors.gray6,
|
||||
borderDark: colors.gray5,
|
||||
icon: colors.gray5,
|
||||
|
||||
// non-standard
|
||||
|
@ -284,6 +290,7 @@ export const darkTheme: Theme = {
|
|||
textInverted: colors.white,
|
||||
link: colors.blue3,
|
||||
border: colors.gray3,
|
||||
borderDark: colors.gray4,
|
||||
icon: colors.gray1,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -62,7 +62,7 @@ export const Menu = observer(
|
|||
onPress?: () => void
|
||||
}) => (
|
||||
<TouchableOpacity
|
||||
testID="menuItemButton"
|
||||
testID={`menuItemButton-${label}`}
|
||||
style={styles.menuItem}
|
||||
onPress={onPress ? onPress : () => onNavigate(url || '/')}>
|
||||
<View style={[styles.menuItemIconWrapper]}>
|
||||
|
|
|
@ -5,7 +5,6 @@ import {
|
|||
Easing,
|
||||
FlatList,
|
||||
GestureResponderEvent,
|
||||
SafeAreaView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
|
@ -16,7 +15,6 @@ import {
|
|||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {ScreenContainer, Screen} from 'react-native-screens'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||
|
@ -34,7 +32,7 @@ import {Text} from '../../com/util/text/Text'
|
|||
import {ErrorBoundary} from '../../com/util/ErrorBoundary'
|
||||
import {TabsSelector} from './TabsSelector'
|
||||
import {Composer} from './Composer'
|
||||
import {s, colors} from '../../lib/styles'
|
||||
import {colors} from '../../lib/styles'
|
||||
import {clamp} from '../../../lib/numbers'
|
||||
import {
|
||||
GridIcon,
|
||||
|
@ -323,18 +321,10 @@ export const MobileShell: React.FC = observer(() => {
|
|||
|
||||
if (!store.session.hasSession) {
|
||||
return (
|
||||
<LinearGradient
|
||||
colors={['#007CFF', '#00BCFF']}
|
||||
start={{x: 0, y: 0.8}}
|
||||
end={{x: 0, y: 1}}
|
||||
style={styles.outerContainer}>
|
||||
<SafeAreaView testID="noSessionView" style={styles.innerContainer}>
|
||||
<ErrorBoundary>
|
||||
<Login />
|
||||
</ErrorBoundary>
|
||||
</SafeAreaView>
|
||||
<View style={styles.outerContainer}>
|
||||
<Login />
|
||||
<Modal />
|
||||
</LinearGradient>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
if (store.onboard.isOnboarding) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue