Rework account creation and login views
parent
d55780f5c3
commit
acf0f80de2
|
@ -65,6 +65,7 @@
|
||||||
"js-sha256": "^0.9.0",
|
"js-sha256": "^0.9.0",
|
||||||
"lodash.chunk": "^4.2.0",
|
"lodash.chunk": "^4.2.0",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"lodash.omit": "^4.5.0",
|
"lodash.omit": "^4.5.0",
|
||||||
"lodash.samplesize": "^4.2.0",
|
"lodash.samplesize": "^4.2.0",
|
||||||
|
@ -122,6 +123,7 @@
|
||||||
"@types/jest": "^29.4.0",
|
"@types/jest": "^29.4.0",
|
||||||
"@types/lodash.chunk": "^4.2.7",
|
"@types/lodash.chunk": "^4.2.7",
|
||||||
"@types/lodash.clonedeep": "^4.5.7",
|
"@types/lodash.clonedeep": "^4.5.7",
|
||||||
|
"@types/lodash.debounce": "^4.0.7",
|
||||||
"@types/lodash.isequal": "^4.5.6",
|
"@types/lodash.isequal": "^4.5.6",
|
||||||
"@types/lodash.omit": "^4.5.7",
|
"@types/lodash.omit": "^4.5.7",
|
||||||
"@types/lodash.samplesize": "^4.2.7",
|
"@types/lodash.samplesize": "^4.2.7",
|
||||||
|
|
|
@ -801,3 +801,30 @@ export function SquarePlusIcon({
|
||||||
</Svg>
|
</Svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function InfoCircleIcon({
|
||||||
|
style,
|
||||||
|
size,
|
||||||
|
strokeWidth = 1.5,
|
||||||
|
}: {
|
||||||
|
style?: StyleProp<TextStyle>
|
||||||
|
size?: string | number
|
||||||
|
strokeWidth?: number
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Svg
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
stroke="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
style={style}>
|
||||||
|
<Path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -64,6 +64,7 @@ export const s = StyleSheet.create({
|
||||||
footerSpacer: {height: 100},
|
footerSpacer: {height: 100},
|
||||||
contentContainer: {paddingBottom: 200},
|
contentContainer: {paddingBottom: 200},
|
||||||
contentContainerExtra: {paddingBottom: 300},
|
contentContainerExtra: {paddingBottom: 300},
|
||||||
|
border0: {borderWidth: 0},
|
||||||
border1: {borderWidth: 1},
|
border1: {borderWidth: 1},
|
||||||
borderTop1: {borderTopWidth: 1},
|
borderTop1: {borderTopWidth: 1},
|
||||||
borderRight1: {borderRightWidth: 1},
|
borderRight1: {borderRightWidth: 1},
|
||||||
|
|
|
@ -6,7 +6,7 @@ import * as apiPolyfill from 'lib/api/api-polyfill'
|
||||||
import * as storage from 'lib/storage'
|
import * as storage from 'lib/storage'
|
||||||
|
|
||||||
export const LOCAL_DEV_SERVICE =
|
export const LOCAL_DEV_SERVICE =
|
||||||
Platform.OS === 'ios' ? 'http://localhost:2583' : 'http://10.0.2.2:2583'
|
Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583'
|
||||||
export const STAGING_SERVICE = 'https://pds.staging.bsky.dev'
|
export const STAGING_SERVICE = 'https://pds.staging.bsky.dev'
|
||||||
export const PROD_SERVICE = 'https://bsky.social'
|
export const PROD_SERVICE = 'https://bsky.social'
|
||||||
export const DEFAULT_SERVICE = PROD_SERVICE
|
export const DEFAULT_SERVICE = PROD_SERVICE
|
||||||
|
|
|
@ -0,0 +1,192 @@
|
||||||
|
import {makeAutoObservable} from 'mobx'
|
||||||
|
import {RootStoreModel} from '../root-store'
|
||||||
|
import {ServiceDescription} from '../session'
|
||||||
|
import {DEFAULT_SERVICE} from 'state/index'
|
||||||
|
import {ComAtprotoAccountCreate} from '@atproto/api'
|
||||||
|
import * as EmailValidator from 'email-validator'
|
||||||
|
import {createFullHandle} from 'lib/strings/handles'
|
||||||
|
import {cleanError} from 'lib/strings/errors'
|
||||||
|
|
||||||
|
export class CreateAccountModel {
|
||||||
|
step: number = 1
|
||||||
|
isProcessing = false
|
||||||
|
isFetchingServiceDescription = false
|
||||||
|
didServiceDescriptionFetchFail = false
|
||||||
|
error = ''
|
||||||
|
|
||||||
|
serviceUrl = DEFAULT_SERVICE
|
||||||
|
serviceDescription: ServiceDescription | undefined = undefined
|
||||||
|
userDomain = ''
|
||||||
|
inviteCode = ''
|
||||||
|
email = ''
|
||||||
|
password = ''
|
||||||
|
handle = ''
|
||||||
|
is13 = false
|
||||||
|
|
||||||
|
constructor(public rootStore: RootStoreModel) {
|
||||||
|
makeAutoObservable(this, {}, {autoBind: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// form state controls
|
||||||
|
// =
|
||||||
|
|
||||||
|
next() {
|
||||||
|
this.error = ''
|
||||||
|
this.step++
|
||||||
|
}
|
||||||
|
|
||||||
|
back() {
|
||||||
|
this.error = ''
|
||||||
|
this.step--
|
||||||
|
}
|
||||||
|
|
||||||
|
setStep(v: number) {
|
||||||
|
this.step = v
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchServiceDescription() {
|
||||||
|
this.setError('')
|
||||||
|
this.setIsFetchingServiceDescription(true)
|
||||||
|
this.setDidServiceDescriptionFetchFail(false)
|
||||||
|
this.setServiceDescription(undefined)
|
||||||
|
if (!this.serviceUrl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const desc = await this.rootStore.session.describeService(this.serviceUrl)
|
||||||
|
this.setServiceDescription(desc)
|
||||||
|
this.setUserDomain(desc.availableUserDomains[0])
|
||||||
|
} catch (err: any) {
|
||||||
|
this.rootStore.log.warn(
|
||||||
|
`Failed to fetch service description for ${this.serviceUrl}`,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
this.setError(
|
||||||
|
'Unable to contact your service. Please check your Internet connection.',
|
||||||
|
)
|
||||||
|
this.setDidServiceDescriptionFetchFail(true)
|
||||||
|
} finally {
|
||||||
|
this.setIsFetchingServiceDescription(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
if (!this.email) {
|
||||||
|
this.setStep(2)
|
||||||
|
return this.setError('Please enter your email.')
|
||||||
|
}
|
||||||
|
if (!EmailValidator.validate(this.email)) {
|
||||||
|
this.setStep(2)
|
||||||
|
return this.setError('Your email appears to be invalid.')
|
||||||
|
}
|
||||||
|
if (!this.password) {
|
||||||
|
this.setStep(2)
|
||||||
|
return this.setError('Please choose your password.')
|
||||||
|
}
|
||||||
|
if (!this.handle) {
|
||||||
|
this.setStep(3)
|
||||||
|
return this.setError('Please choose your handle.')
|
||||||
|
}
|
||||||
|
this.setError('')
|
||||||
|
this.setIsProcessing(true)
|
||||||
|
try {
|
||||||
|
await this.rootStore.session.createAccount({
|
||||||
|
service: this.serviceUrl,
|
||||||
|
email: this.email,
|
||||||
|
handle: createFullHandle(this.handle, this.userDomain),
|
||||||
|
password: this.password,
|
||||||
|
inviteCode: this.inviteCode,
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
let errMsg = e.toString()
|
||||||
|
if (e instanceof ComAtprotoAccountCreate.InvalidInviteCodeError) {
|
||||||
|
errMsg =
|
||||||
|
'Invite code not accepted. Check that you input it correctly and try again.'
|
||||||
|
}
|
||||||
|
this.rootStore.log.error('Failed to create account', e)
|
||||||
|
this.setIsProcessing(false)
|
||||||
|
this.setError(cleanError(errMsg))
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// form state accessors
|
||||||
|
// =
|
||||||
|
|
||||||
|
get canBack() {
|
||||||
|
return this.step > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get canNext() {
|
||||||
|
if (this.step === 1) {
|
||||||
|
return !!this.serviceDescription
|
||||||
|
} else if (this.step === 2) {
|
||||||
|
return (
|
||||||
|
(!this.isInviteCodeRequired || this.inviteCode) &&
|
||||||
|
!!this.email &&
|
||||||
|
!!this.password &&
|
||||||
|
this.is13
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return !!this.handle
|
||||||
|
}
|
||||||
|
|
||||||
|
get isServiceDescribed() {
|
||||||
|
return !!this.serviceDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
get isInviteCodeRequired() {
|
||||||
|
return this.serviceDescription?.inviteCodeRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
// setters
|
||||||
|
// =
|
||||||
|
|
||||||
|
setIsProcessing(v: boolean) {
|
||||||
|
this.isProcessing = v
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsFetchingServiceDescription(v: boolean) {
|
||||||
|
this.isFetchingServiceDescription = v
|
||||||
|
}
|
||||||
|
|
||||||
|
setDidServiceDescriptionFetchFail(v: boolean) {
|
||||||
|
this.didServiceDescriptionFetchFail = v
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(v: string) {
|
||||||
|
this.error = v
|
||||||
|
}
|
||||||
|
|
||||||
|
setServiceUrl(v: string) {
|
||||||
|
this.serviceUrl = v
|
||||||
|
}
|
||||||
|
|
||||||
|
setServiceDescription(v: ServiceDescription | undefined) {
|
||||||
|
this.serviceDescription = v
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserDomain(v: string) {
|
||||||
|
this.userDomain = v
|
||||||
|
}
|
||||||
|
|
||||||
|
setInviteCode(v: string) {
|
||||||
|
this.inviteCode = v
|
||||||
|
}
|
||||||
|
|
||||||
|
setEmail(v: string) {
|
||||||
|
this.email = v
|
||||||
|
}
|
||||||
|
|
||||||
|
setPassword(v: string) {
|
||||||
|
this.password = v
|
||||||
|
}
|
||||||
|
|
||||||
|
setHandle(v: string) {
|
||||||
|
this.handle = v
|
||||||
|
}
|
||||||
|
|
||||||
|
setIs13(v: boolean) {
|
||||||
|
this.is13 = v
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {SafeAreaView} from 'react-native'
|
import {SafeAreaView} from 'react-native'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {Signin} from 'view/com/auth/Signin'
|
import {Login} from 'view/com/auth/login/Login'
|
||||||
import {CreateAccount} from 'view/com/auth/CreateAccount'
|
import {CreateAccount} from 'view/com/auth/create/CreateAccount'
|
||||||
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
@ -12,8 +12,8 @@ import {SplashScreen} from './SplashScreen'
|
||||||
import {CenteredView} from '../util/Views'
|
import {CenteredView} from '../util/Views'
|
||||||
|
|
||||||
enum ScreenState {
|
enum ScreenState {
|
||||||
S_SigninOrCreateAccount,
|
S_LoginOrCreateAccount,
|
||||||
S_Signin,
|
S_Login,
|
||||||
S_CreateAccount,
|
S_CreateAccount,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ export const LoggedOut = observer(() => {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const {screen} = useAnalytics()
|
const {screen} = useAnalytics()
|
||||||
const [screenState, setScreenState] = React.useState<ScreenState>(
|
const [screenState, setScreenState] = React.useState<ScreenState>(
|
||||||
ScreenState.S_SigninOrCreateAccount,
|
ScreenState.S_LoginOrCreateAccount,
|
||||||
)
|
)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -32,11 +32,11 @@ export const LoggedOut = observer(() => {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
store.session.isResumingSession ||
|
store.session.isResumingSession ||
|
||||||
screenState === ScreenState.S_SigninOrCreateAccount
|
screenState === ScreenState.S_LoginOrCreateAccount
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<SplashScreen
|
<SplashScreen
|
||||||
onPressSignin={() => setScreenState(ScreenState.S_Signin)}
|
onPressSignin={() => setScreenState(ScreenState.S_Login)}
|
||||||
onPressCreateAccount={() => setScreenState(ScreenState.S_CreateAccount)}
|
onPressCreateAccount={() => setScreenState(ScreenState.S_CreateAccount)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -46,17 +46,17 @@ export const LoggedOut = observer(() => {
|
||||||
<CenteredView style={[s.hContentRegion, pal.view]}>
|
<CenteredView style={[s.hContentRegion, pal.view]}>
|
||||||
<SafeAreaView testID="noSessionView" style={s.hContentRegion}>
|
<SafeAreaView testID="noSessionView" style={s.hContentRegion}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{screenState === ScreenState.S_Signin ? (
|
{screenState === ScreenState.S_Login ? (
|
||||||
<Signin
|
<Login
|
||||||
onPressBack={() =>
|
onPressBack={() =>
|
||||||
setScreenState(ScreenState.S_SigninOrCreateAccount)
|
setScreenState(ScreenState.S_LoginOrCreateAccount)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
{screenState === ScreenState.S_CreateAccount ? (
|
{screenState === ScreenState.S_CreateAccount ? (
|
||||||
<CreateAccount
|
<CreateAccount
|
||||||
onPressBack={() =>
|
onPressBack={() =>
|
||||||
setScreenState(ScreenState.S_SigninOrCreateAccount)
|
setScreenState(ScreenState.S_LoginOrCreateAccount)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native'
|
import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||||
import Image, {Source as ImageSource} from 'view/com/util/images/Image'
|
|
||||||
import {Text} from 'view/com/util/text/Text'
|
import {Text} from 'view/com/util/text/Text'
|
||||||
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
||||||
import {colors} from 'lib/styles'
|
import {s, colors} from 'lib/styles'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {CLOUD_SPLASH} from 'lib/assets'
|
|
||||||
import {CenteredView} from '../util/Views'
|
import {CenteredView} from '../util/Views'
|
||||||
|
|
||||||
export const SplashScreen = ({
|
export const SplashScreen = ({
|
||||||
|
@ -17,29 +15,29 @@ export const SplashScreen = ({
|
||||||
}) => {
|
}) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
return (
|
return (
|
||||||
<CenteredView style={styles.container}>
|
<CenteredView style={[styles.container, pal.view]}>
|
||||||
<Image source={CLOUD_SPLASH as ImageSource} style={styles.bgImg} />
|
|
||||||
<SafeAreaView testID="noSessionView" style={styles.container}>
|
<SafeAreaView testID="noSessionView" style={styles.container}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<View style={styles.hero}>
|
<View style={styles.hero}>
|
||||||
<View style={styles.heroText}>
|
<Text style={[styles.title, pal.link]}>Bluesky</Text>
|
||||||
<Text style={styles.title}>Bluesky</Text>
|
<Text style={[styles.subtitle, pal.textLight]}>
|
||||||
</View>
|
See what's next
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View testID="signinOrCreateAccount" style={styles.btns}>
|
<View testID="signinOrCreateAccount" style={styles.btns}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="createAccountButton"
|
testID="createAccountButton"
|
||||||
style={[pal.view, styles.btn]}
|
style={[styles.btn, {backgroundColor: colors.blue3}]}
|
||||||
onPress={onPressCreateAccount}>
|
onPress={onPressCreateAccount}>
|
||||||
<Text style={[pal.link, styles.btnLabel]}>
|
<Text style={[s.white, styles.btnLabel]}>
|
||||||
Create a new account
|
Create a new account
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="signInButton"
|
testID="signInButton"
|
||||||
style={[pal.view, styles.btn]}
|
style={[styles.btn, pal.btn]}
|
||||||
onPress={onPressSignin}>
|
onPress={onPressSignin}>
|
||||||
<Text style={[pal.link, styles.btnLabel]}>Sign in</Text>
|
<Text style={[pal.text, styles.btnLabel]}>Sign in</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
@ -56,37 +54,27 @@ const styles = StyleSheet.create({
|
||||||
flex: 2,
|
flex: 2,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
bgImg: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
},
|
|
||||||
heroText: {
|
|
||||||
backgroundColor: colors.white,
|
|
||||||
paddingTop: 10,
|
|
||||||
paddingBottom: 20,
|
|
||||||
},
|
|
||||||
btns: {
|
btns: {
|
||||||
paddingBottom: 40,
|
paddingBottom: 40,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
color: colors.blue3,
|
|
||||||
fontSize: 68,
|
fontSize: 68,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
},
|
},
|
||||||
|
subtitle: {
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: 42,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
btn: {
|
btn: {
|
||||||
borderRadius: 4,
|
borderRadius: 32,
|
||||||
paddingVertical: 16,
|
paddingVertical: 16,
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
marginHorizontal: 20,
|
marginHorizontal: 20,
|
||||||
backgroundColor: colors.blue3,
|
|
||||||
},
|
},
|
||||||
btnLabel: {
|
btnLabel: {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontSize: 21,
|
fontSize: 21,
|
||||||
color: colors.white,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -17,10 +17,10 @@ import {ComAtprotoAccountCreate} from '@atproto/api'
|
||||||
import * as EmailValidator from 'email-validator'
|
import * as EmailValidator from 'email-validator'
|
||||||
import {sha256} from 'js-sha256'
|
import {sha256} from 'js-sha256'
|
||||||
import {useAnalytics} from 'lib/analytics'
|
import {useAnalytics} from 'lib/analytics'
|
||||||
import {LogoTextHero} from './Logo'
|
import {LogoTextHero} from '../Logo'
|
||||||
import {Picker} from '../util/Picker'
|
import {Picker} from '../../util/Picker'
|
||||||
import {TextLink} from '../util/Link'
|
import {TextLink} from '../../util/Link'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../../util/text/Text'
|
||||||
import {s, colors} from 'lib/styles'
|
import {s, colors} from 'lib/styles'
|
||||||
import {makeValidHandle, createFullHandle} from 'lib/strings/handles'
|
import {makeValidHandle, createFullHandle} from 'lib/strings/handles'
|
||||||
import {toNiceDomain} from 'lib/strings/url-helpers'
|
import {toNiceDomain} from 'lib/strings/url-helpers'
|
|
@ -0,0 +1,241 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from 'react-native'
|
||||||
|
import {observer} from 'mobx-react-lite'
|
||||||
|
import {sha256} from 'js-sha256'
|
||||||
|
import {useAnalytics} from 'lib/analytics'
|
||||||
|
import {Text} from '../../util/text/Text'
|
||||||
|
import {s, colors} from 'lib/styles'
|
||||||
|
import {useStores} from 'state/index'
|
||||||
|
import {CreateAccountModel} from 'state/models/ui/create-account'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
|
|
||||||
|
import {Step1} from './Step1'
|
||||||
|
import {Step2} from './Step2'
|
||||||
|
import {Step3} from './Step3'
|
||||||
|
|
||||||
|
export const CreateAccount = observer(
|
||||||
|
({onPressBack}: {onPressBack: () => void}) => {
|
||||||
|
const {track, screen, identify} = useAnalytics()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const store = useStores()
|
||||||
|
const model = React.useMemo(() => new CreateAccountModel(store), [store])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
screen('CreateAccount')
|
||||||
|
}, [screen])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
model.fetchServiceDescription()
|
||||||
|
}, [model])
|
||||||
|
|
||||||
|
const onPressRetryConnect = React.useCallback(
|
||||||
|
() => model.fetchServiceDescription(),
|
||||||
|
[model],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onPressBackInner = React.useCallback(() => {
|
||||||
|
if (model.canBack) {
|
||||||
|
console.log('?')
|
||||||
|
model.back()
|
||||||
|
} else {
|
||||||
|
onPressBack()
|
||||||
|
}
|
||||||
|
}, [model, onPressBack])
|
||||||
|
|
||||||
|
const onPressNext = React.useCallback(async () => {
|
||||||
|
if (!model.canNext) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (model.step < 3) {
|
||||||
|
model.next()
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await model.submit()
|
||||||
|
const email_hashed = sha256(model.email)
|
||||||
|
identify(email_hashed, {email_hashed})
|
||||||
|
track('Create Account')
|
||||||
|
} catch {
|
||||||
|
// dont need to handle here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [model, identify, track])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView testID="createAccount" style={pal.view}>
|
||||||
|
<KeyboardAvoidingView behavior="padding">
|
||||||
|
<View style={styles.stepContainer}>
|
||||||
|
{model.step === 1 && <Step1 model={model} />}
|
||||||
|
{model.step === 2 && <Step2 model={model} />}
|
||||||
|
{model.step === 3 && <Step3 model={model} />}
|
||||||
|
</View>
|
||||||
|
<View style={[s.flexRow, s.pl20, s.pr20]}>
|
||||||
|
<TouchableOpacity onPress={onPressBackInner}>
|
||||||
|
<Text type="xl" style={pal.link}>
|
||||||
|
Back
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={s.flex1} />
|
||||||
|
{model.canNext ? (
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="createAccountButton"
|
||||||
|
onPress={onPressNext}>
|
||||||
|
{model.isProcessing ? (
|
||||||
|
<ActivityIndicator />
|
||||||
|
) : (
|
||||||
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
|
Next
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : model.didServiceDescriptionFetchFail ? (
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="registerRetryButton"
|
||||||
|
onPress={onPressRetryConnect}>
|
||||||
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
|
Retry
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : model.isFetchingServiceDescription ? (
|
||||||
|
<>
|
||||||
|
<ActivityIndicator color="#fff" />
|
||||||
|
<Text type="xl" style={[pal.text, s.pr5]}>
|
||||||
|
Connecting...
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
) : undefined}
|
||||||
|
</View>
|
||||||
|
<View style={s.footerSpacer} />
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</ScrollView>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
stepContainer: {
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingVertical: 20,
|
||||||
|
},
|
||||||
|
|
||||||
|
noTopBorder: {
|
||||||
|
borderTopWidth: 0,
|
||||||
|
},
|
||||||
|
logoHero: {
|
||||||
|
paddingTop: 30,
|
||||||
|
paddingBottom: 40,
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 10,
|
||||||
|
marginBottom: 20,
|
||||||
|
marginHorizontal: 20,
|
||||||
|
},
|
||||||
|
groupLabel: {
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingBottom: 5,
|
||||||
|
},
|
||||||
|
groupContent: {
|
||||||
|
borderTopWidth: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
groupContentIcon: {
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
textInput: {
|
||||||
|
flex: 1,
|
||||||
|
width: '100%',
|
||||||
|
paddingVertical: 10,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
fontSize: 17,
|
||||||
|
letterSpacing: 0.25,
|
||||||
|
fontWeight: '400',
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
textBtn: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
textBtnLabel: {
|
||||||
|
flex: 1,
|
||||||
|
paddingVertical: 10,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
},
|
||||||
|
textBtnFakeInnerBtn: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 6,
|
||||||
|
paddingVertical: 6,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
marginHorizontal: 6,
|
||||||
|
},
|
||||||
|
textBtnFakeInnerBtnIcon: {
|
||||||
|
marginRight: 4,
|
||||||
|
},
|
||||||
|
picker: {
|
||||||
|
flex: 1,
|
||||||
|
width: '100%',
|
||||||
|
paddingVertical: 10,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
fontSize: 17,
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
pickerLabel: {
|
||||||
|
fontSize: 17,
|
||||||
|
},
|
||||||
|
checkbox: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 2,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
marginLeft: 16,
|
||||||
|
},
|
||||||
|
checkboxFilled: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 2,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
marginLeft: 16,
|
||||||
|
},
|
||||||
|
policies: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingBottom: 20,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
backgroundColor: colors.red4,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: -5,
|
||||||
|
marginHorizontal: 20,
|
||||||
|
marginBottom: 15,
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
errorFloating: {
|
||||||
|
marginBottom: 20,
|
||||||
|
marginHorizontal: 20,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
errorIcon: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.white,
|
||||||
|
borderRadius: 30,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginRight: 5,
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,101 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {StyleSheet, View} from 'react-native'
|
||||||
|
import {
|
||||||
|
FontAwesomeIcon,
|
||||||
|
FontAwesomeIconStyle,
|
||||||
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {TextLink} from '../../util/Link'
|
||||||
|
import {Text} from '../../util/text/Text'
|
||||||
|
import {s, colors} from 'lib/styles'
|
||||||
|
import {ServiceDescription} from 'state/models/session'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
|
||||||
|
export 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 as FontAwesomeIconStyle}
|
||||||
|
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
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
policies: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
},
|
||||||
|
errorIcon: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.white,
|
||||||
|
borderRadius: 30,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginRight: 5,
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,187 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
|
||||||
|
import {observer} from 'mobx-react-lite'
|
||||||
|
import debounce from 'lodash.debounce'
|
||||||
|
import {Text} from 'view/com/util/text/Text'
|
||||||
|
import {StepHeader} from './StepHeader'
|
||||||
|
import {CreateAccountModel} from 'state/models/ui/create-account'
|
||||||
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {s} from 'lib/styles'
|
||||||
|
import {HelpTip} from '../util/HelpTip'
|
||||||
|
import {TextInput} from '../util/TextInput'
|
||||||
|
import {Button} from 'view/com/util/forms/Button'
|
||||||
|
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
|
||||||
|
|
||||||
|
import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index'
|
||||||
|
import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags'
|
||||||
|
|
||||||
|
export const Step1 = observer(({model}: {model: CreateAccountModel}) => {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const [isDefaultSelected, setIsDefaultSelected] = React.useState(true)
|
||||||
|
|
||||||
|
const onPressDefault = React.useCallback(() => {
|
||||||
|
setIsDefaultSelected(true)
|
||||||
|
model.setServiceUrl(PROD_SERVICE)
|
||||||
|
model.fetchServiceDescription()
|
||||||
|
}, [setIsDefaultSelected, model])
|
||||||
|
|
||||||
|
const onPressOther = React.useCallback(() => {
|
||||||
|
setIsDefaultSelected(false)
|
||||||
|
model.setServiceUrl('https://')
|
||||||
|
model.setServiceDescription(undefined)
|
||||||
|
}, [setIsDefaultSelected, model])
|
||||||
|
|
||||||
|
const fetchServiceDesription = React.useMemo(
|
||||||
|
() => debounce(() => model.fetchServiceDescription(), 1e3),
|
||||||
|
[model],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onChangeServiceUrl = React.useCallback(
|
||||||
|
(v: string) => {
|
||||||
|
model.setServiceUrl(v)
|
||||||
|
fetchServiceDesription()
|
||||||
|
},
|
||||||
|
[model, fetchServiceDesription],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onDebugChangeServiceUrl = React.useCallback(
|
||||||
|
(v: string) => {
|
||||||
|
model.setServiceUrl(v)
|
||||||
|
model.fetchServiceDescription()
|
||||||
|
},
|
||||||
|
[model],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<StepHeader step="1" title="Your hosting provider" />
|
||||||
|
<Text style={[pal.text, s.mb10]}>
|
||||||
|
This is the company that keeps you online.
|
||||||
|
</Text>
|
||||||
|
<Option
|
||||||
|
isSelected={isDefaultSelected}
|
||||||
|
label="Bluesky"
|
||||||
|
help=" (default)"
|
||||||
|
onPress={onPressDefault}
|
||||||
|
/>
|
||||||
|
<Option
|
||||||
|
isSelected={!isDefaultSelected}
|
||||||
|
label="Other"
|
||||||
|
onPress={onPressOther}>
|
||||||
|
<View style={styles.otherForm}>
|
||||||
|
<Text style={[pal.text, s.mb5]}>
|
||||||
|
Enter the address of your provider:
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
icon="globe"
|
||||||
|
placeholder="Hosting provider address"
|
||||||
|
value={model.serviceUrl}
|
||||||
|
editable
|
||||||
|
onChange={onChangeServiceUrl}
|
||||||
|
/>
|
||||||
|
{LOGIN_INCLUDE_DEV_SERVERS && (
|
||||||
|
<View style={[s.flexRow, s.mt10]}>
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
style={s.mr5}
|
||||||
|
label="Staging"
|
||||||
|
onPress={() => onDebugChangeServiceUrl(STAGING_SERVICE)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
label="Dev Server"
|
||||||
|
onPress={() => onDebugChangeServiceUrl(LOCAL_DEV_SERVICE)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Option>
|
||||||
|
{model.error ? (
|
||||||
|
<ErrorMessage message={model.error} style={styles.error} />
|
||||||
|
) : (
|
||||||
|
<HelpTip text="You can change hosting providers at any time." />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function Option({
|
||||||
|
children,
|
||||||
|
isSelected,
|
||||||
|
label,
|
||||||
|
help,
|
||||||
|
onPress,
|
||||||
|
}: React.PropsWithChildren<{
|
||||||
|
isSelected: boolean
|
||||||
|
label: string
|
||||||
|
help?: string
|
||||||
|
onPress: () => void
|
||||||
|
}>) {
|
||||||
|
const theme = useTheme()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const circleFillStyle = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
backgroundColor: theme.palette.primary.background,
|
||||||
|
}),
|
||||||
|
[theme],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.option, pal.border]}>
|
||||||
|
<TouchableWithoutFeedback onPress={onPress}>
|
||||||
|
<View style={styles.optionHeading}>
|
||||||
|
<View style={[styles.circle, pal.border]}>
|
||||||
|
{isSelected ? (
|
||||||
|
<View style={[circleFillStyle, styles.circleFill]} />
|
||||||
|
) : undefined}
|
||||||
|
</View>
|
||||||
|
<Text type="xl" style={pal.text}>
|
||||||
|
{label}
|
||||||
|
{help ? (
|
||||||
|
<Text type="xl" style={pal.textLight}>
|
||||||
|
{help}
|
||||||
|
</Text>
|
||||||
|
) : undefined}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
{isSelected && children}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
error: {
|
||||||
|
borderRadius: 6,
|
||||||
|
},
|
||||||
|
|
||||||
|
option: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 6,
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
optionHeading: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 10,
|
||||||
|
},
|
||||||
|
circle: {
|
||||||
|
width: 26,
|
||||||
|
height: 26,
|
||||||
|
borderRadius: 15,
|
||||||
|
padding: 4,
|
||||||
|
borderWidth: 1,
|
||||||
|
marginRight: 10,
|
||||||
|
},
|
||||||
|
circleFill: {
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
|
||||||
|
otherForm: {
|
||||||
|
paddingBottom: 10,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,275 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||||
|
import {observer} from 'mobx-react-lite'
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {CreateAccountModel} from 'state/models/ui/create-account'
|
||||||
|
import {Text} from 'view/com/util/text/Text'
|
||||||
|
import {TextLink} from 'view/com/util/Link'
|
||||||
|
import {StepHeader} from './StepHeader'
|
||||||
|
import {s} from 'lib/styles'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {TextInput} from '../util/TextInput'
|
||||||
|
import {Policies} from './Policies'
|
||||||
|
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
|
||||||
|
|
||||||
|
export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<StepHeader step="2" title="Your account" />
|
||||||
|
|
||||||
|
{model.isInviteCodeRequired && (
|
||||||
|
<View style={s.pb20}>
|
||||||
|
<Text type="md-medium" style={[pal.text, s.mb2]}>
|
||||||
|
Invite code
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
icon="ticket"
|
||||||
|
placeholder="Required for this provider"
|
||||||
|
value={model.inviteCode}
|
||||||
|
editable
|
||||||
|
onChange={model.setInviteCode}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!model.inviteCode && model.isInviteCodeRequired ? (
|
||||||
|
<Text>
|
||||||
|
Don't have an invite code?{' '}
|
||||||
|
<TextLink text="Join the waitlist" href="#" style={pal.link} /> to try
|
||||||
|
the beta before it's publicly available.
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<View style={s.pb20}>
|
||||||
|
<Text type="md-medium" style={[pal.text, s.mb2]}>
|
||||||
|
Email address
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
icon="envelope"
|
||||||
|
placeholder="Enter your email address"
|
||||||
|
value={model.email}
|
||||||
|
editable
|
||||||
|
onChange={model.setEmail}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={s.pb20}>
|
||||||
|
<Text type="md-medium" style={[pal.text, s.mb2]}>
|
||||||
|
Password
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
icon="lock"
|
||||||
|
placeholder="Choose your password"
|
||||||
|
value={model.password}
|
||||||
|
editable
|
||||||
|
secureTextEntry
|
||||||
|
onChange={model.setPassword}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={s.pb20}>
|
||||||
|
<Text type="md-medium" style={[pal.text, s.mb2]}>
|
||||||
|
Legal check
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
testID="registerIs13Input"
|
||||||
|
style={[styles.toggleBtn, pal.border]}
|
||||||
|
onPress={() => model.setIs13(!model.is13)}>
|
||||||
|
<View style={[pal.borderDark, styles.checkbox]}>
|
||||||
|
{model.is13 && (
|
||||||
|
<FontAwesomeIcon icon="check" style={s.blue3} size={16} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text type="md" style={[pal.text, styles.toggleBtnLabel]}>
|
||||||
|
I am 13 years old or older
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{model.serviceDescription && (
|
||||||
|
<Policies serviceDescription={model.serviceDescription} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{model.error ? (
|
||||||
|
<ErrorMessage message={model.error} style={styles.error} />
|
||||||
|
) : undefined}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
error: {
|
||||||
|
borderRadius: 6,
|
||||||
|
marginTop: 10,
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleBtn: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
borderWidth: 1,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 10,
|
||||||
|
borderRadius: 6,
|
||||||
|
},
|
||||||
|
toggleBtnLabel: {
|
||||||
|
flex: 1,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
},
|
||||||
|
|
||||||
|
checkbox: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 2,
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
<View style={[pal.borderDark, styles.group]}>
|
||||||
|
{serviceDescription?.inviteCodeRequired ? (
|
||||||
|
<View
|
||||||
|
style={[pal.border, styles.groupContent, styles.noTopBorder]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="ticket"
|
||||||
|
style={[pal.textLight, styles.groupContentIcon]}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
style={[pal.text, styles.textInput]}
|
||||||
|
placeholder="Invite code"
|
||||||
|
placeholderTextColor={pal.colors.textLight}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
autoFocus
|
||||||
|
keyboardAppearance={theme.colorScheme}
|
||||||
|
value={inviteCode}
|
||||||
|
onChangeText={setInviteCode}
|
||||||
|
onBlur={onBlurInviteCode}
|
||||||
|
editable={!isProcessing}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : undefined}
|
||||||
|
<View style={[pal.border, styles.groupContent]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="envelope"
|
||||||
|
style={[pal.textLight, styles.groupContentIcon]}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
testID="registerEmailInput"
|
||||||
|
style={[pal.text, styles.textInput]}
|
||||||
|
placeholder="Email address"
|
||||||
|
placeholderTextColor={pal.colors.textLight}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
value={email}
|
||||||
|
onChangeText={setEmail}
|
||||||
|
editable={!isProcessing}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={[pal.border, styles.groupContent]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="lock"
|
||||||
|
style={[pal.textLight, styles.groupContentIcon]}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
testID="registerPasswordInput"
|
||||||
|
style={[pal.text, styles.textInput]}
|
||||||
|
placeholder="Choose your password"
|
||||||
|
placeholderTextColor={pal.colors.textLight}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
secureTextEntry
|
||||||
|
value={password}
|
||||||
|
onChangeText={setPassword}
|
||||||
|
editable={!isProcessing}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
) : undefined}
|
||||||
|
{serviceDescription ? (
|
||||||
|
<>
|
||||||
|
<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={[pal.text, styles.textInput]}
|
||||||
|
placeholder="eg alice"
|
||||||
|
placeholderTextColor={pal.colors.textLight}
|
||||||
|
autoCapitalize="none"
|
||||||
|
value={handle}
|
||||||
|
onChangeText={v => setHandle(makeValidHandle(v))}
|
||||||
|
editable={!isProcessing}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
{serviceDescription.availableUserDomains.length > 1 && (
|
||||||
|
<View style={[pal.border, styles.groupContent]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="globe"
|
||||||
|
style={styles.groupContentIcon}
|
||||||
|
/>
|
||||||
|
<Picker
|
||||||
|
style={[pal.text, styles.picker]}
|
||||||
|
labelStyle={styles.pickerLabel}
|
||||||
|
iconStyle={pal.textLight as FontAwesomeIconStyle}
|
||||||
|
value={userDomain}
|
||||||
|
items={serviceDescription.availableUserDomains.map(d => ({
|
||||||
|
label: `.${d}`,
|
||||||
|
value: d,
|
||||||
|
}))}
|
||||||
|
onChange={itemValue => setUserDomain(itemValue)}
|
||||||
|
enabled={!isProcessing}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View style={[pal.border, styles.groupContent]}>
|
||||||
|
<Text style={[pal.textLight, s.p10]}>
|
||||||
|
Your full username will be{' '}
|
||||||
|
<Text type="md-bold" style={pal.textLight}>
|
||||||
|
@{createFullHandle(handle, userDomain)}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<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={[
|
||||||
|
pal.border,
|
||||||
|
is13 ? styles.checkboxFilled : styles.checkbox,
|
||||||
|
]}>
|
||||||
|
{is13 && (
|
||||||
|
<FontAwesomeIcon icon="check" style={s.blue3} size={14} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text style={[pal.text, styles.textBtnLabel]}>
|
||||||
|
I am 13 years old or older
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>*/
|
|
@ -0,0 +1,44 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {StyleSheet, View} from 'react-native'
|
||||||
|
import {observer} from 'mobx-react-lite'
|
||||||
|
import {CreateAccountModel} from 'state/models/ui/create-account'
|
||||||
|
import {Text} from 'view/com/util/text/Text'
|
||||||
|
import {StepHeader} from './StepHeader'
|
||||||
|
import {s} from 'lib/styles'
|
||||||
|
import {TextInput} from '../util/TextInput'
|
||||||
|
import {createFullHandle} from 'lib/strings/handles'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
|
||||||
|
|
||||||
|
export const Step3 = observer(({model}: {model: CreateAccountModel}) => {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<StepHeader step="3" title="Your user handle" />
|
||||||
|
<View style={s.pb10}>
|
||||||
|
<TextInput
|
||||||
|
icon="at"
|
||||||
|
placeholder="eg alice"
|
||||||
|
value={model.handle}
|
||||||
|
editable
|
||||||
|
onChange={model.setHandle}
|
||||||
|
/>
|
||||||
|
<Text type="lg" style={[pal.text, s.pl5, s.pt10]}>
|
||||||
|
Your full handle will be{' '}
|
||||||
|
<Text type="lg-bold" style={pal.text}>
|
||||||
|
@{createFullHandle(model.handle, model.userDomain)}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{model.error ? (
|
||||||
|
<ErrorMessage message={model.error} style={styles.error} />
|
||||||
|
) : undefined}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
error: {
|
||||||
|
borderRadius: 6,
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {StyleSheet, View} from 'react-native'
|
||||||
|
import {Text} from 'view/com/util/text/Text'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
|
||||||
|
export function StepHeader({step, title}: {step: string; title: string}) {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text type="lg" style={pal.textLight}>
|
||||||
|
{step === '3' ? 'Last step!' : <>Step {step} of 3</>}
|
||||||
|
</Text>
|
||||||
|
<Text type="title-xl">{title}</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
})
|
|
@ -15,9 +15,8 @@ import {
|
||||||
import * as EmailValidator from 'email-validator'
|
import * as EmailValidator from 'email-validator'
|
||||||
import AtpAgent from '@atproto/api'
|
import AtpAgent from '@atproto/api'
|
||||||
import {useAnalytics} from 'lib/analytics'
|
import {useAnalytics} from 'lib/analytics'
|
||||||
import {LogoTextHero} from './Logo'
|
import {Text} from '../../util/text/Text'
|
||||||
import {Text} from '../util/text/Text'
|
import {UserAvatar} from '../../util/UserAvatar'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
|
||||||
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'
|
||||||
|
@ -37,7 +36,7 @@ enum Forms {
|
||||||
PasswordUpdated,
|
PasswordUpdated,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
|
export const Login = ({onPressBack}: {onPressBack: () => void}) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
|
@ -100,7 +99,10 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView testID="signIn" behavior="padding" style={[pal.view]}>
|
<KeyboardAvoidingView
|
||||||
|
testID="signIn"
|
||||||
|
behavior="padding"
|
||||||
|
style={[pal.view, s.pt10]}>
|
||||||
{currentForm === Forms.Login ? (
|
{currentForm === Forms.Login ? (
|
||||||
<LoginForm
|
<LoginForm
|
||||||
store={store}
|
store={store}
|
||||||
|
@ -164,9 +166,9 @@ const ChooseAccountForm = ({
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const [isProcessing, setIsProcessing] = React.useState(false)
|
const [isProcessing, setIsProcessing] = React.useState(false)
|
||||||
|
|
||||||
// React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
screen('Choose Account')
|
screen('Choose Account')
|
||||||
// }, [screen])
|
}, [screen])
|
||||||
|
|
||||||
const onTryAccount = async (account: AccountData) => {
|
const onTryAccount = async (account: AccountData) => {
|
||||||
if (account.accessJwt && account.refreshJwt) {
|
if (account.accessJwt && account.refreshJwt) {
|
||||||
|
@ -183,15 +185,16 @@ const ChooseAccountForm = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View testID="chooseAccountForm">
|
<View testID="chooseAccountForm">
|
||||||
<LogoTextHero />
|
<Text
|
||||||
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
|
type="2xl-medium"
|
||||||
|
style={[pal.text, styles.groupLabel, s.mt5, s.mb10]}>
|
||||||
Sign in as...
|
Sign in as...
|
||||||
</Text>
|
</Text>
|
||||||
{store.session.accounts.map(account => (
|
{store.session.accounts.map(account => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID={`chooseAccountBtn-${account.handle}`}
|
testID={`chooseAccountBtn-${account.handle}`}
|
||||||
key={account.did}
|
key={account.did}
|
||||||
style={[pal.borderDark, styles.group, s.mb5]}
|
style={[pal.view, pal.border, styles.account]}
|
||||||
onPress={() => onTryAccount(account)}>
|
onPress={() => onTryAccount(account)}>
|
||||||
<View
|
<View
|
||||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||||
|
@ -216,7 +219,7 @@ const ChooseAccountForm = ({
|
||||||
))}
|
))}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="chooseNewAccountBtn"
|
testID="chooseNewAccountBtn"
|
||||||
style={[pal.borderDark, styles.group]}
|
style={[pal.view, pal.border, styles.account, styles.accountLast]}
|
||||||
onPress={() => onSelectAccount(undefined)}>
|
onPress={() => onSelectAccount(undefined)}>
|
||||||
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
||||||
<Text style={[styles.accountText, styles.accountTextOther]}>
|
<Text style={[styles.accountText, styles.accountTextOther]}>
|
||||||
|
@ -336,7 +339,6 @@ const LoginForm = ({
|
||||||
const isReady = !!serviceDescription && !!identifier && !!password
|
const isReady = !!serviceDescription && !!identifier && !!password
|
||||||
return (
|
return (
|
||||||
<View testID="loginForm">
|
<View testID="loginForm">
|
||||||
<LogoTextHero />
|
|
||||||
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
|
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
|
||||||
Sign into
|
Sign into
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -523,7 +525,6 @@ const ForgotPasswordForm = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LogoTextHero />
|
|
||||||
<View>
|
<View>
|
||||||
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
||||||
Reset password
|
Reset password
|
||||||
|
@ -669,7 +670,6 @@ const SetNewPasswordForm = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LogoTextHero />
|
|
||||||
<View>
|
<View>
|
||||||
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
||||||
Set new password
|
Set new password
|
||||||
|
@ -774,7 +774,6 @@ const PasswordUpdatedForm = ({onPressNext}: {onPressNext: () => void}) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LogoTextHero />
|
|
||||||
<View>
|
<View>
|
||||||
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
||||||
Password updated!
|
Password updated!
|
||||||
|
@ -825,6 +824,16 @@ const styles = StyleSheet.create({
|
||||||
groupContentIcon: {
|
groupContentIcon: {
|
||||||
marginLeft: 10,
|
marginLeft: 10,
|
||||||
},
|
},
|
||||||
|
account: {
|
||||||
|
borderTopWidth: 1,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingVertical: 4,
|
||||||
|
},
|
||||||
|
accountLast: {
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
marginBottom: 20,
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
textInput: {
|
textInput: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
width: '100%',
|
width: '100%',
|
|
@ -0,0 +1,32 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {StyleSheet, View} from 'react-native'
|
||||||
|
import {Text} from 'view/com/util/text/Text'
|
||||||
|
import {InfoCircleIcon} from 'lib/icons'
|
||||||
|
import {s, colors} from 'lib/styles'
|
||||||
|
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
|
||||||
|
|
||||||
|
export function HelpTip({text}: {text: string}) {
|
||||||
|
const bg = useColorSchemeStyle(
|
||||||
|
{backgroundColor: colors.gray1},
|
||||||
|
{backgroundColor: colors.gray8},
|
||||||
|
)
|
||||||
|
const fg = useColorSchemeStyle({color: colors.gray5}, {color: colors.gray4})
|
||||||
|
return (
|
||||||
|
<View style={[styles.helptip, bg]}>
|
||||||
|
<InfoCircleIcon size={18} style={fg} strokeWidth={1.5} />
|
||||||
|
<Text type="xs-medium" style={[fg, s.ml5]}>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
helptip: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 6,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
})
|
|
@ -0,0 +1,68 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {StyleSheet, TextInput as RNTextInput, View} from 'react-native'
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
|
|
||||||
|
export function TextInput({
|
||||||
|
testID,
|
||||||
|
icon,
|
||||||
|
value,
|
||||||
|
placeholder,
|
||||||
|
editable,
|
||||||
|
secureTextEntry,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
testID?: string
|
||||||
|
icon: IconProp
|
||||||
|
value: string
|
||||||
|
placeholder: string
|
||||||
|
editable: boolean
|
||||||
|
secureTextEntry?: boolean
|
||||||
|
onChange: (v: string) => void
|
||||||
|
}) {
|
||||||
|
const theme = useTheme()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
return (
|
||||||
|
<View style={[pal.border, styles.container]}>
|
||||||
|
<FontAwesomeIcon icon={icon} style={[pal.textLight, styles.icon]} />
|
||||||
|
<RNTextInput
|
||||||
|
testID={testID}
|
||||||
|
style={[pal.text, styles.textInput]}
|
||||||
|
placeholder={placeholder}
|
||||||
|
placeholderTextColor={pal.colors.textLight}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
keyboardAppearance={theme.colorScheme}
|
||||||
|
secureTextEntry={secureTextEntry}
|
||||||
|
value={value}
|
||||||
|
onChangeText={v => onChange(v)}
|
||||||
|
editable={editable}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 6,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 4,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
textInput: {
|
||||||
|
flex: 1,
|
||||||
|
width: '100%',
|
||||||
|
paddingVertical: 10,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
fontSize: 17,
|
||||||
|
letterSpacing: 0.25,
|
||||||
|
fontWeight: '400',
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
})
|
|
@ -5,7 +5,7 @@ import {
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {BottomSheetTextInput} from '@gorhom/bottom-sheet'
|
import {TextInput} from './util'
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
|
@ -116,7 +116,7 @@ export function Component({}: {}) {
|
||||||
Check your inbox for an email with the confirmation code to enter
|
Check your inbox for an email with the confirmation code to enter
|
||||||
below:
|
below:
|
||||||
</Text>
|
</Text>
|
||||||
<BottomSheetTextInput
|
<TextInput
|
||||||
style={[styles.textInput, pal.borderDark, pal.text, styles.mb20]}
|
style={[styles.textInput, pal.borderDark, pal.text, styles.mb20]}
|
||||||
placeholder="Confirmation code"
|
placeholder="Confirmation code"
|
||||||
placeholderTextColor={pal.textLight.color}
|
placeholderTextColor={pal.textLight.color}
|
||||||
|
@ -127,7 +127,7 @@ export function Component({}: {}) {
|
||||||
<Text type="lg" style={styles.description}>
|
<Text type="lg" style={styles.description}>
|
||||||
Please enter your password as well:
|
Please enter your password as well:
|
||||||
</Text>
|
</Text>
|
||||||
<BottomSheetTextInput
|
<TextInput
|
||||||
style={[styles.textInput, pal.borderDark, pal.text]}
|
style={[styles.textInput, pal.borderDark, pal.text]}
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
placeholderTextColor={pal.textLight.color}
|
placeholderTextColor={pal.textLight.color}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import * as EditProfileModal from './EditProfile'
|
||||||
import * as ServerInputModal from './ServerInput'
|
import * as ServerInputModal from './ServerInput'
|
||||||
import * as ReportPostModal from './ReportPost'
|
import * as ReportPostModal from './ReportPost'
|
||||||
import * as ReportAccountModal from './ReportAccount'
|
import * as ReportAccountModal from './ReportAccount'
|
||||||
|
import * as DeleteAccountModal from './DeleteAccount'
|
||||||
import * as RepostModal from './Repost'
|
import * as RepostModal from './Repost'
|
||||||
import * as CropImageModal from './crop-image/CropImage.web'
|
import * as CropImageModal from './crop-image/CropImage.web'
|
||||||
import * as ChangeHandleModal from './ChangeHandle'
|
import * as ChangeHandleModal from './ChangeHandle'
|
||||||
|
@ -61,6 +62,8 @@ function Modal({modal}: {modal: ModalIface}) {
|
||||||
element = <ReportAccountModal.Component {...modal} />
|
element = <ReportAccountModal.Component {...modal} />
|
||||||
} else if (modal.name === 'crop-image') {
|
} else if (modal.name === 'crop-image') {
|
||||||
element = <CropImageModal.Component {...modal} />
|
element = <CropImageModal.Component {...modal} />
|
||||||
|
} else if (modal.name === 'delete-account') {
|
||||||
|
element = <DeleteAccountModal.Component />
|
||||||
} else if (modal.name === 'repost') {
|
} else if (modal.name === 'repost') {
|
||||||
element = <RepostModal.Component {...modal} />
|
element = <RepostModal.Component {...modal} />
|
||||||
} else if (modal.name === 'change-handle') {
|
} else if (modal.name === 'change-handle') {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {useStores} from 'state/index'
|
||||||
import {SUGGESTED_FOLLOWS} from 'lib/constants'
|
import {SUGGESTED_FOLLOWS} from 'lib/constants'
|
||||||
// @ts-ignore no type definition -prf
|
// @ts-ignore no type definition -prf
|
||||||
import ProgressBar from 'react-native-progress/Bar'
|
import ProgressBar from 'react-native-progress/Bar'
|
||||||
|
import {CenteredView} from './Views'
|
||||||
|
|
||||||
export const WelcomeBanner = observer(() => {
|
export const WelcomeBanner = observer(() => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
@ -39,7 +40,7 @@ export const WelcomeBanner = observer(() => {
|
||||||
}, [store])
|
}, [store])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<CenteredView
|
||||||
testID="welcomeBanner"
|
testID="welcomeBanner"
|
||||||
style={[pal.view, styles.container, pal.border]}>
|
style={[pal.view, styles.container, pal.border]}>
|
||||||
<Text
|
<Text
|
||||||
|
@ -76,7 +77,7 @@ export const WelcomeBanner = observer(() => {
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
</CenteredView>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ export function ErrorMessage({
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Text
|
<Text
|
||||||
type="sm"
|
type="sm-medium"
|
||||||
style={[styles.message, pal.text]}
|
style={[styles.message, pal.text]}
|
||||||
numberOfLines={numberOfLines}>
|
numberOfLines={numberOfLines}>
|
||||||
{message}
|
{message}
|
||||||
|
|
|
@ -3508,6 +3508,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/lodash" "*"
|
"@types/lodash" "*"
|
||||||
|
|
||||||
|
"@types/lodash.debounce@^4.0.7":
|
||||||
|
version "4.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.7.tgz#0285879defb7cdb156ae633cecd62d5680eded9f"
|
||||||
|
integrity sha512-X1T4wMZ+gT000M2/91SYj0d/7JfeNZ9PeeOldSNoE/lunLeQXKvkmIumI29IaKMotU/ln/McOIvgzZcQ/3TrSA==
|
||||||
|
dependencies:
|
||||||
|
"@types/lodash" "*"
|
||||||
|
|
||||||
"@types/lodash.isequal@^4.5.6":
|
"@types/lodash.isequal@^4.5.6":
|
||||||
version "4.5.6"
|
version "4.5.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/lodash.isequal/-/lodash.isequal-4.5.6.tgz#ff42a1b8e20caa59a97e446a77dc57db923bc02b"
|
resolved "https://registry.yarnpkg.com/@types/lodash.isequal/-/lodash.isequal-4.5.6.tgz#ff42a1b8e20caa59a97e446a77dc57db923bc02b"
|
||||||
|
|
Loading…
Reference in New Issue