Add account creation

This commit is contained in:
Paul Frazee 2022-09-27 14:24:47 -05:00
parent c89ec94b17
commit ef4b9cf8d9
17 changed files with 727 additions and 272 deletions

View file

@ -1,8 +1,11 @@
import {makeAutoObservable} from 'mobx'
import AdxApi from '../../third-party/api'
import type * as GetAccountsConfig from '../../third-party/api/src/types/todo/adx/getAccountsConfig'
import {isObj, hasProp} from '../lib/type-guards'
import {RootStoreModel} from './root-store'
export type ServiceDescription = GetAccountsConfig.OutputSchema
interface SessionData {
service: string
token: string
@ -10,8 +13,17 @@ interface SessionData {
userdid: string
}
export enum OnboardingStage {
Init = 'init',
}
interface OnboardingState {
stage: OnboardingStage
}
export class SessionModel {
data: SessionData | null = null
onboardingState: OnboardingState | null = null
constructor(public rootStore: RootStoreModel) {
makeAutoObservable(this, {
@ -26,31 +38,51 @@ export class SessionModel {
}
serialize(): unknown {
return this.data
return {
data: this.data,
onboardingState: this.onboardingState,
}
}
hydrate(v: unknown) {
if (isObj(v)) {
const data: SessionData = {
service: '',
token: '',
username: '',
userdid: '',
if (hasProp(v, 'data') && isObj(v.data)) {
const data: SessionData = {
service: '',
token: '',
username: '',
userdid: '',
}
if (hasProp(v.data, 'service') && typeof v.data.service === 'string') {
data.service = v.data.service
}
if (hasProp(v.data, 'token') && typeof v.data.token === 'string') {
data.token = v.data.token
}
if (
hasProp(v.data, 'username') &&
typeof v.data.username === 'string'
) {
data.username = v.data.username
}
if (hasProp(v.data, 'userdid') && typeof v.data.userdid === 'string') {
data.userdid = v.data.userdid
}
if (data.service && data.token && data.username && data.userdid) {
this.data = data
}
}
if (hasProp(v, 'service') && typeof v.service === 'string') {
data.service = v.service
}
if (hasProp(v, 'token') && typeof v.token === 'string') {
data.token = v.token
}
if (hasProp(v, 'username') && typeof v.username === 'string') {
data.username = v.username
}
if (hasProp(v, 'userdid') && typeof v.userdid === 'string') {
data.userdid = v.userdid
}
if (data.service && data.token && data.username && data.userdid) {
this.data = data
if (
this.data &&
hasProp(v, 'onboardingState') &&
isObj(v.onboardingState)
) {
if (
hasProp(v.onboardingState, 'stage') &&
typeof v.onboardingState === 'string'
) {
this.onboardingState = v.onboardingState
}
}
}
}
@ -100,6 +132,12 @@ export class SessionModel {
this.clear() // invalid session cached
}
async describeService(service: string): Promise<ServiceDescription> {
const api = AdxApi.service(service)
const res = await api.todo.adx.getAccountsConfig({})
return res.data
}
async login({
service,
username,
@ -122,6 +160,36 @@ export class SessionModel {
}
}
async createAccount({
service,
email,
password,
username,
inviteCode,
}: {
service: string
email: string
password: string
username: string
inviteCode?: string
}) {
const api = AdxApi.service(service)
const res = await api.todo.adx.createAccount(
{},
{username, password, email, inviteCode},
)
if (res.data.jwt) {
this.setState({
service: service,
token: res.data.jwt,
username: res.data.name,
userdid: res.data.did,
})
this.setOnboardingStage(OnboardingStage.Init)
this.configureApi()
}
}
async logout() {
if (this.isAuthed) {
this.rootStore.api.todo.adx.deleteSession({}).catch((e: any) => {
@ -130,4 +198,12 @@ export class SessionModel {
}
this.clear()
}
setOnboardingStage(stage: OnboardingStage | null) {
if (stage === null) {
this.onboardingState = null
} else {
this.onboardingState = {stage}
}
}
}

View file

@ -10253,12 +10253,15 @@ var methodSchemas = [
encoding: "application/json",
schema: {
type: "object",
required: ["username", "did", "password"],
required: ["email", "username", "password"],
properties: {
email: {
type: "string"
},
username: {
type: "string"
},
did: {
inviteCode: {
type: "string"
},
password: {
@ -10271,10 +10274,16 @@ var methodSchemas = [
encoding: "application/json",
schema: {
type: "object",
required: ["jwt"],
required: ["jwt", "name", "did"],
properties: {
jwt: {
type: "string"
},
name: {
type: "string"
},
did: {
type: "string"
}
}
}
@ -10305,10 +10314,16 @@ var methodSchemas = [
encoding: "application/json",
schema: {
type: "object",
required: ["jwt"],
required: ["jwt", "name", "did"],
properties: {
jwt: {
type: "string"
},
name: {
type: "string"
},
did: {
type: "string"
}
}
}
@ -10359,16 +10374,37 @@ var methodSchemas = [
schema: {}
}
},
{
lexicon: 1,
id: "todo.adx.getAccountsConfig",
type: "query",
description: "Get a document describing the service's accounts configuration.",
parameters: {},
output: {
encoding: "application/json",
schema: {
type: "object",
required: ["availableUserDomains"],
properties: {
inviteCodeRequired: {
type: "boolean"
},
availableUserDomains: {
type: "array",
items: {
type: "string"
}
}
}
}
}
},
{
lexicon: 1,
id: "todo.adx.getSession",
type: "query",
description: "Get information about the current session.",
parameters: {},
input: {
encoding: "",
schema: {}
},
output: {
encoding: "application/json",
schema: {
@ -11603,6 +11639,14 @@ var AdxNS = class {
getAccount(params, data, opts) {
return this._service.xrpc.call("todo.adx.getAccount", params, data, opts);
}
getAccountsConfig(params, data, opts) {
return this._service.xrpc.call(
"todo.adx.getAccountsConfig",
params,
data,
opts
);
}
getSession(params, data, opts) {
return this._service.xrpc.call("todo.adx.getSession", params, data, opts);
}

File diff suppressed because one or more lines are too long

View file

@ -4,6 +4,7 @@ import * as TodoAdxCreateSession from './types/todo/adx/createSession';
import * as TodoAdxDeleteAccount from './types/todo/adx/deleteAccount';
import * as TodoAdxDeleteSession from './types/todo/adx/deleteSession';
import * as TodoAdxGetAccount from './types/todo/adx/getAccount';
import * as TodoAdxGetAccountsConfig from './types/todo/adx/getAccountsConfig';
import * as TodoAdxGetSession from './types/todo/adx/getSession';
import * as TodoAdxRepoBatchWrite from './types/todo/adx/repoBatchWrite';
import * as TodoAdxRepoCreateRecord from './types/todo/adx/repoCreateRecord';
@ -59,6 +60,7 @@ export declare class AdxNS {
deleteAccount(params: TodoAdxDeleteAccount.QueryParams, data?: TodoAdxDeleteAccount.InputSchema, opts?: TodoAdxDeleteAccount.CallOptions): Promise<TodoAdxDeleteAccount.Response>;
deleteSession(params: TodoAdxDeleteSession.QueryParams, data?: TodoAdxDeleteSession.InputSchema, opts?: TodoAdxDeleteSession.CallOptions): Promise<TodoAdxDeleteSession.Response>;
getAccount(params: TodoAdxGetAccount.QueryParams, data?: TodoAdxGetAccount.InputSchema, opts?: TodoAdxGetAccount.CallOptions): Promise<TodoAdxGetAccount.Response>;
getAccountsConfig(params: TodoAdxGetAccountsConfig.QueryParams, data?: TodoAdxGetAccountsConfig.InputSchema, opts?: TodoAdxGetAccountsConfig.CallOptions): Promise<TodoAdxGetAccountsConfig.Response>;
getSession(params: TodoAdxGetSession.QueryParams, data?: TodoAdxGetSession.InputSchema, opts?: TodoAdxGetSession.CallOptions): Promise<TodoAdxGetSession.Response>;
repoBatchWrite(params: TodoAdxRepoBatchWrite.QueryParams, data?: TodoAdxRepoBatchWrite.InputSchema, opts?: TodoAdxRepoBatchWrite.CallOptions): Promise<TodoAdxRepoBatchWrite.Response>;
repoCreateRecord(params: TodoAdxRepoCreateRecord.QueryParams, data?: TodoAdxRepoCreateRecord.InputSchema, opts?: TodoAdxRepoCreateRecord.CallOptions): Promise<TodoAdxRepoCreateRecord.Response>;

View file

@ -6,12 +6,15 @@ export interface CallOptions {
encoding: 'application/json';
}
export interface InputSchema {
email: string;
username: string;
did: string;
inviteCode?: string;
password: string;
}
export interface OutputSchema {
jwt: string;
name: string;
did: string;
}
export interface Response {
success: boolean;

View file

@ -11,6 +11,8 @@ export interface InputSchema {
}
export interface OutputSchema {
jwt: string;
name: string;
did: string;
}
export interface Response {
success: boolean;

View file

@ -0,0 +1,17 @@
import { Headers } from '@adxp/xrpc';
export interface QueryParams {
}
export interface CallOptions {
headers?: Headers;
}
export declare type InputSchema = undefined;
export interface OutputSchema {
inviteCodeRequired?: boolean;
availableUserDomains: string[];
}
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}

View file

@ -3,11 +3,8 @@ export interface QueryParams {
}
export interface CallOptions {
headers?: Headers;
encoding: '';
}
export interface InputSchema {
[k: string]: unknown;
}
export declare type InputSchema = undefined;
export interface OutputSchema {
name: string;
did: string;

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,155 @@
import React, {useRef} from 'react'
import {
StyleProp,
StyleSheet,
Text,
TextStyle,
TouchableOpacity,
TouchableWithoutFeedback,
View,
ViewStyle,
} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import RootSiblings from 'react-native-root-siblings'
import {colors} from '../../lib/styles'
interface PickerItem {
value: string
label: string
}
interface PickerOpts {
style?: StyleProp<ViewStyle>
labelStyle?: StyleProp<TextStyle>
iconStyle?: FontAwesomeIconStyle
items: PickerItem[]
value: string
onChange: (value: string) => void
enabled?: boolean
}
const MENU_WIDTH = 200
export function Picker({
style,
labelStyle,
iconStyle,
items,
value,
onChange,
enabled,
}: PickerOpts) {
const ref = useRef<View>(null)
const valueLabel = items.find(item => item.value === value)?.label || value
const onPress = () => {
if (!enabled) {
return
}
ref.current?.measure(
(
_x: number,
_y: number,
width: number,
height: number,
pageX: number,
pageY: number,
) => {
createDropdownMenu(pageX, pageY + height, MENU_WIDTH, items, onChange)
},
)
}
return (
<TouchableWithoutFeedback onPress={onPress}>
<View style={[styles.outer, style]} ref={ref}>
<View style={styles.label}>
<Text style={labelStyle}>{valueLabel}</Text>
</View>
<FontAwesomeIcon icon="angle-down" style={[styles.icon, iconStyle]} />
</View>
</TouchableWithoutFeedback>
)
}
function createDropdownMenu(
x: number,
y: number,
width: number,
items: PickerItem[],
onChange: (value: string) => void,
): RootSiblings {
const onPressItem = (index: number) => {
sibling.destroy()
onChange(items[index].value)
}
const onOuterPress = () => sibling.destroy()
const sibling = new RootSiblings(
(
<>
<TouchableWithoutFeedback onPress={onOuterPress}>
<View style={styles.bg} />
</TouchableWithoutFeedback>
<View style={[styles.menu, {left: x, top: y, width}]}>
{items.map((item, index) => (
<TouchableOpacity
key={index}
style={[styles.menuItem, index !== 0 && styles.menuItemBorder]}
onPress={() => onPressItem(index)}>
<Text style={styles.menuItemLabel}>{item.label}</Text>
</TouchableOpacity>
))}
</View>
</>
),
)
return sibling
}
const styles = StyleSheet.create({
outer: {
flexDirection: 'row',
alignItems: 'center',
},
label: {
marginRight: 5,
},
icon: {},
bg: {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
backgroundColor: '#000',
opacity: 0.1,
},
menu: {
position: 'absolute',
backgroundColor: '#fff',
borderRadius: 14,
opacity: 1,
paddingVertical: 6,
},
menuItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 6,
paddingLeft: 15,
paddingRight: 30,
},
menuItemBorder: {
borderTopWidth: 1,
borderTopColor: colors.gray2,
marginTop: 4,
paddingTop: 12,
},
menuItemIcon: {
marginLeft: 6,
marginRight: 8,
},
menuItemLabel: {
fontSize: 15,
},
})

View file

@ -1,5 +1,6 @@
import {library} from '@fortawesome/fontawesome-svg-core'
import {faAngleDown} from '@fortawesome/free-solid-svg-icons/faAngleDown'
import {faAngleLeft} from '@fortawesome/free-solid-svg-icons/faAngleLeft'
import {faAngleRight} from '@fortawesome/free-solid-svg-icons/faAngleRight'
import {faArrowLeft} from '@fortawesome/free-solid-svg-icons/faArrowLeft'
@ -7,6 +8,7 @@ import {faArrowRightFromBracket} from '@fortawesome/free-solid-svg-icons'
import {faArrowUpFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket'
import {faArrowUpRightFromSquare} from '@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare'
import {faArrowsRotate} from '@fortawesome/free-solid-svg-icons/faArrowsRotate'
import {faAt} from '@fortawesome/free-solid-svg-icons/faAt'
import {faBars} from '@fortawesome/free-solid-svg-icons/faBars'
import {faBell} from '@fortawesome/free-solid-svg-icons/faBell'
import {faBell as farBell} from '@fortawesome/free-regular-svg-icons/faBell'
@ -16,12 +18,15 @@ import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck'
import {faClone} from '@fortawesome/free-regular-svg-icons/faClone'
import {faComment} from '@fortawesome/free-regular-svg-icons/faComment'
import {faEllipsis} from '@fortawesome/free-solid-svg-icons/faEllipsis'
import {faEnvelope} from '@fortawesome/free-solid-svg-icons/faEnvelope'
import {faExclamation} from '@fortawesome/free-solid-svg-icons/faExclamation'
import {faGear} from '@fortawesome/free-solid-svg-icons/faGear'
import {faGlobe} from '@fortawesome/free-solid-svg-icons/faGlobe'
import {faHeart} from '@fortawesome/free-regular-svg-icons/faHeart'
import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart'
import {faHouse} from '@fortawesome/free-solid-svg-icons/faHouse'
import {faLink} from '@fortawesome/free-solid-svg-icons/faLink'
import {faLock} from '@fortawesome/free-solid-svg-icons/faLock'
import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass'
import {faMessage} from '@fortawesome/free-regular-svg-icons/faMessage'
import {faPenNib} from '@fortawesome/free-solid-svg-icons/faPenNib'
@ -32,10 +37,12 @@ import {faShield} from '@fortawesome/free-solid-svg-icons/faShield'
import {faRetweet} from '@fortawesome/free-solid-svg-icons/faRetweet'
import {faUser} from '@fortawesome/free-regular-svg-icons/faUser'
import {faUsers} from '@fortawesome/free-solid-svg-icons/faUsers'
import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket'
import {faX} from '@fortawesome/free-solid-svg-icons/faX'
export function setup() {
library.add(
faAngleDown,
faAngleLeft,
faAngleRight,
faArrowLeft,
@ -43,6 +50,7 @@ export function setup() {
faArrowUpFromBracket,
faArrowUpRightFromSquare,
faArrowsRotate,
faAt,
faBars,
faBell,
farBell,
@ -52,12 +60,15 @@ export function setup() {
faClone,
faComment,
faEllipsis,
faEnvelope,
faExclamation,
faGear,
faGlobe,
faHeart,
fasHeart,
faHouse,
faLink,
faLock,
faMagnifyingGlass,
faMessage,
faPenNib,
@ -68,6 +79,7 @@ export function setup() {
faShield,
faUser,
faUsers,
faTicket,
faX,
)
}

View file

@ -11,6 +11,7 @@ export const colors = {
gray4: '#968d8d',
gray5: '#645454',
blue0: '#bfe1ff',
blue1: '#8bc7fd',
blue2: '#52acfe',
blue3: '#0085ff',

View file

@ -1,4 +1,4 @@
import React, {useState} from 'react'
import React, {useState, useEffect} from 'react'
import {
ActivityIndicator,
KeyboardAvoidingView,
@ -11,85 +11,69 @@ import {
} from 'react-native'
import Svg, {Circle, Line, Text as SvgText} from 'react-native-svg'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import * as EmailValidator from 'email-validator'
import {observer} from 'mobx-react-lite'
import {Picker} from '../com/util/Picker'
import {s, colors} from '../lib/styles'
import {useStores} from '../../state'
import {ServiceDescription} from '../../state/models/session'
enum ScreenState {
SigninOrCreateAccount,
Signin,
CreateAccount,
}
const Logo = () => {
return (
<View style={styles.logo}>
<Svg width="100" height="100">
<Circle
cx="50"
cy="50"
r="46"
fill="none"
stroke="white"
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" />
<SvgText
fill="none"
stroke="white"
strokeWidth={2}
fontSize="60"
fontWeight="bold"
x="52"
y="70"
textAnchor="middle">
B
</SvgText>
</Svg>
</View>
)
}
const SigninOrCreateAccount = ({
onPressSignin,
onPressCreateAccount,
}: {
onPressSignin: () => void
onPressCreateAccount: () => void
}) => {
const winDim = useWindowDimensions()
const halfWidth = winDim.width / 2
return (
<>
<View style={styles.hero}>
<View style={styles.logo}>
<Svg width="100" height="100">
<Circle
cx="50"
cy="50"
r="46"
fill="none"
stroke="white"
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"
/>
<SvgText
fill="none"
stroke="white"
strokeWidth={2}
fontSize="60"
fontWeight="bold"
x="52"
y="70"
textAnchor="middle">
B
</SvgText>
</Svg>
</View>
<Logo />
<Text style={styles.title}>Bluesky</Text>
<Text style={styles.subtitle}>[ private beta ]</Text>
</View>
<View style={s.flex1}>
<TouchableOpacity style={styles.btn}>
<TouchableOpacity style={styles.btn} onPress={onPressCreateAccount}>
<Text style={styles.btnLabel}>Create a new account</Text>
</TouchableOpacity>
<View style={styles.or}>
@ -155,31 +139,221 @@ const Signin = ({onPressBack}: {onPressBack: () => void}) => {
return (
<KeyboardAvoidingView behavior="padding" style={{flex: 1}}>
<View style={styles.smallHero}>
<Text style={styles.title}>Bluesky</Text>
<Text style={styles.subtitle}>[ private beta ]</Text>
<View style={styles.logoHero}>
<Logo />
</View>
<View style={s.flex1}>
<View style={styles.group}>
<View style={styles.groupTitle}>
<Text style={[s.white, s.f18]}>Sign in</Text>
<View style={styles.group}>
<View style={styles.groupTitle}>
<Text style={[s.white, s.f18, s.bold]}>Sign in</Text>
</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>
<View style={styles.groupContent}>
<View style={[s.mb5]}>
) : undefined}
<View style={styles.groupContent}>
<FontAwesomeIcon icon="envelope" style={styles.groupContentIcon} />
<TextInput
style={styles.textInput}
placeholder="Email or username"
placeholderTextColor={colors.blue0}
autoCapitalize="none"
autoFocus
value={username}
onChangeText={setUsername}
editable={!isProcessing}
/>
</View>
<View style={styles.groupContent}>
<FontAwesomeIcon icon="lock" style={styles.groupContentIcon} />
<TextInput
style={styles.textInput}
placeholder="Password"
placeholderTextColor={colors.blue0}
autoCapitalize="none"
secureTextEntry
value={password}
onChangeText={setPassword}
editable={!isProcessing}
/>
</View>
</View>
<View style={[s.flexRow, s.pl20, s.pr20]}>
<TouchableOpacity onPress={onPressBack}>
<Text style={[s.white, s.f18, s.pl5]}>Back</Text>
</TouchableOpacity>
<View style={s.flex1} />
<TouchableOpacity onPress={onPressNext}>
{isProcessing ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text>
)}
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
)
}
const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
const store = useStores()
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [error, setError] = useState<string>('')
const [serviceDescription, setServiceDescription] = useState<
ServiceDescription | undefined
>(undefined)
const [userDomain, setUserDomain] = useState<string>('')
const [inviteCode, setInviteCode] = useState<string>('')
const [email, setEmail] = useState<string>('')
const [password, setPassword] = useState<string>('')
const [username, setUsername] = useState<string>('')
useEffect(() => {
if (serviceDescription || error) {
return
}
store.session.describeService('http://localhost:2583/').then(
desc => {
setServiceDescription(desc)
setUserDomain(desc.availableUserDomains[0])
},
err => {
console.error(err)
setError(
'Unable to contact your service. Please check your Internet connection.',
)
},
)
}, [])
const onPressNext = async () => {
if (!email) {
return setError('Please enter your email.')
}
if (!EmailValidator.validate(email)) {
return setError('Your email appears to be invalid.')
}
if (!password) {
return setError('Please choose your password.')
}
if (!username) {
return setError('Please choose your username.')
}
setError('')
setIsProcessing(true)
try {
await store.session.createAccount({
service: 'http://localhost:2583/',
email,
username: `${username}.${userDomain}`,
password,
inviteCode,
})
} catch (e: any) {
const errMsg = e.toString()
console.log(e)
setIsProcessing(false)
// if (errMsg.includes('Authentication Required')) {
// setError('Invalid username or password')
// } else if (errMsg.includes('Network request failed')) {
// setError(
// 'Unable to contact your service. Please check your Internet connection.',
// )
// } else {
setError(errMsg.replace(/^Error:/, ''))
// }
}
}
const InitialLoadView = () => (
<>
{error ? (
<>
<View style={[styles.error, styles.errorFloating]}>
<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>
<View style={[s.flexRow, s.pl20, s.pr20]}>
<TouchableOpacity onPress={onPressBack}>
<Text style={[s.white, s.f18, s.pl5]}>Back</Text>
</TouchableOpacity>
</View>
</>
) : (
<ActivityIndicator color="#fff" />
)}
</>
)
return (
<KeyboardAvoidingView behavior="padding" style={{flex: 1}}>
<View style={styles.logoHero}>
<Logo />
</View>
{serviceDescription ? (
<>
{error ? (
<View style={[styles.error, styles.errorFloating]}>
<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={styles.group}>
<View style={styles.groupTitle}>
<Text style={[s.white, s.f18, s.bold]}>Create a new account</Text>
</View>
{serviceDescription?.inviteCodeRequired ? (
<View style={styles.groupContent}>
<FontAwesomeIcon
icon="ticket"
style={styles.groupContentIcon}
/>
<TextInput
style={[styles.textInput]}
placeholder="Invite code"
placeholderTextColor={colors.blue0}
autoCapitalize="none"
autoFocus
value={inviteCode}
onChangeText={setInviteCode}
editable={!isProcessing}
/>
</View>
) : undefined}
<View style={styles.groupContent}>
<FontAwesomeIcon
icon="envelope"
style={styles.groupContentIcon}
/>
<TextInput
style={styles.textInput}
placeholder="Email or username"
style={[styles.textInput]}
placeholder="Email address"
placeholderTextColor={colors.blue0}
autoCapitalize="none"
autoFocus
value={username}
onChangeText={setUsername}
value={email}
onChangeText={setEmail}
editable={!isProcessing}
/>
</View>
<View style={[s.mb5]}>
<View style={styles.groupContent}>
<FontAwesomeIcon icon="lock" style={styles.groupContentIcon} />
<TextInput
style={styles.textInput}
placeholder="Password"
style={[styles.textInput]}
placeholder="Choose your password"
placeholderTextColor={colors.blue0}
autoCapitalize="none"
secureTextEntry
value={password}
@ -187,54 +361,94 @@ const Signin = ({onPressBack}: {onPressBack: () => void}) => {
editable={!isProcessing}
/>
</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>
</View>
<View style={[s.flexRow, s.pl20, s.pr20]}>
<TouchableOpacity onPress={onPressBack}>
<Text style={[s.white, s.f18, s.bold, s.pl5]}>Back</Text>
</TouchableOpacity>
<View style={s.flex1} />
<TouchableOpacity onPress={onPressNext}>
{isProcessing ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text>
<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} />
<TextInput
style={[styles.textInput]}
placeholder="eg alice"
placeholderTextColor={colors.blue0}
autoCapitalize="none"
value={username}
onChangeText={v => setUsername(cleanUsername(v))}
editable={!isProcessing}
/>
</View>
{serviceDescription.availableUserDomains.length > 1 && (
<View style={styles.groupContent}>
<FontAwesomeIcon icon="globe" style={styles.groupContentIcon} />
<Picker
style={styles.picker}
labelStyle={styles.pickerLabel}
iconStyle={styles.pickerIcon}
value={userDomain}
items={serviceDescription.availableUserDomains.map(d => ({
label: `.${d}`,
value: d,
}))}
onChange={itemValue => setUserDomain(itemValue)}
enabled={!isProcessing}
/>
</View>
)}
</TouchableOpacity>
</View>
</View>
<View style={styles.groupContent}>
<Text style={[s.white, s.p10]}>
Your full username will be{' '}
<Text style={s.bold}>
@{username}.{userDomain}
</Text>
</Text>
</View>
</View>
<View style={[s.flexRow, s.pl20, s.pr20]}>
<TouchableOpacity onPress={onPressBack}>
<Text style={[s.white, s.f18, s.pl5]}>Back</Text>
</TouchableOpacity>
<View style={s.flex1} />
<TouchableOpacity onPress={onPressNext}>
{isProcessing ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text>
)}
</TouchableOpacity>
</View>
</>
) : (
<InitialLoadView />
)}
</KeyboardAvoidingView>
)
}
function cleanUsername(v: string): string {
v = v.trim()
if (v.length > 63) {
v = v.slice(0, 63)
}
return v.toLowerCase().replace(/[^a-z0-9-]/g, '')
}
export const Login = observer(
(/*{navigation}: RootTabsScreenProps<'Login'>*/) => {
// const store = useStores()
const [screenState, setScreenState] = useState<ScreenState>(
ScreenState.SigninOrCreateAccount,
)
const onPressSignin = () => {
setScreenState(ScreenState.Signin)
}
return (
<View style={styles.outer}>
{screenState === ScreenState.SigninOrCreateAccount ? (
<SigninOrCreateAccount onPressSignin={onPressSignin} />
<SigninOrCreateAccount
onPressSignin={() => setScreenState(ScreenState.Signin)}
onPressCreateAccount={() =>
setScreenState(ScreenState.CreateAccount)
}
/>
) : undefined}
{screenState === ScreenState.Signin ? (
<Signin
@ -243,6 +457,13 @@ export const Login = observer(
}
/>
) : undefined}
{screenState === ScreenState.CreateAccount ? (
<CreateAccount
onPressBack={() =>
setScreenState(ScreenState.SigninOrCreateAccount)
}
/>
) : undefined}
</View>
)
},
@ -256,9 +477,9 @@ const styles = StyleSheet.create({
flex: 2,
justifyContent: 'center',
},
smallHero: {
flex: 1,
justifyContent: 'center',
logoHero: {
paddingTop: 30,
paddingBottom: 40,
},
logo: {
flexDirection: 'row',
@ -282,6 +503,7 @@ const styles = StyleSheet.create({
paddingVertical: 16,
marginBottom: 20,
marginHorizontal: 20,
backgroundColor: colors.blue3,
},
btnLabel: {
textAlign: 'center',
@ -307,33 +529,65 @@ const styles = StyleSheet.create({
borderRadius: 10,
marginBottom: 20,
marginHorizontal: 20,
backgroundColor: colors.blue3,
},
groupTitle: {
paddingVertical: 8,
paddingHorizontal: 12,
borderBottomWidth: 1,
borderBottomColor: colors.blue1,
},
groupContent: {
paddingVertical: 8,
paddingHorizontal: 12,
borderTopWidth: 1,
borderTopColor: colors.blue1,
flexDirection: 'row',
alignItems: 'center',
},
groupContentIcon: {
color: 'white',
marginLeft: 10,
},
textInput: {
flex: 1,
width: '100%',
backgroundColor: colors.white,
paddingHorizontal: 8,
paddingVertical: 8,
borderRadius: 4,
backgroundColor: colors.blue3,
color: colors.white,
paddingVertical: 10,
paddingHorizontal: 12,
fontSize: 18,
borderRadius: 10,
},
picker: {
flex: 1,
width: '100%',
backgroundColor: colors.blue3,
color: colors.white,
paddingVertical: 10,
paddingHorizontal: 12,
fontSize: 18,
borderRadius: 10,
},
pickerLabel: {
color: colors.white,
fontSize: 18,
},
pickerIcon: {
color: colors.white,
},
error: {
borderTopWidth: 1,
borderTopColor: colors.blue1,
flexDirection: 'row',
alignItems: 'center',
marginTop: 5,
backgroundColor: colors.purple3,
backgroundColor: colors.blue2,
paddingHorizontal: 8,
paddingVertical: 5,
borderRadius: 4,
},
errorFloating: {
borderWidth: 1,
borderColor: colors.blue1,
marginBottom: 20,
marginHorizontal: 20,
borderRadius: 8,
},
errorIcon: {
borderWidth: 1,

View file

@ -12,7 +12,6 @@ import {
} from 'react-native'
import {ScreenContainer, Screen} from 'react-native-screens'
import LinearGradient from 'react-native-linear-gradient'
// import Svg, {Polygon} from 'react-native-svg'
import {GestureDetector, Gesture} from 'react-native-gesture-handler'
import Animated, {
useSharedValue,
@ -33,7 +32,7 @@ import {LocationNavigator} from './location-navigator'
import {createBackMenu, createForwardMenu} from './history-menu'
import {createAccountsMenu} from './accounts-menu'
import {createLocationMenu} from './location-menu'
import {s, colors, gradients} from '../../lib/styles'
import {s, colors} from '../../lib/styles'
import {AVIS} from '../../lib/assets'
const locationIconNeedsNudgeUp = (icon: IconProp) => icon === 'house'
@ -174,40 +173,8 @@ export const MobileShell: React.FC = observer(() => {
<LinearGradient
colors={['#007CFF', '#00BCFF']}
start={{x: 0, y: 0.8}}
end={{x: 1, y: 1}}
end={{x: 0, y: 1}}
style={styles.outerContainer}>
{
undefined /* TODO want this? <Svg height={winDim.height} width={winDim.width} style={s.absolute}>
<Polygon
points={`
${winDim.width},0
${winDim.width - 250},0
0,${winDim.height - 140}
0,${winDim.height}
${winDim.width},${winDim.height}`}
fill="#fff"
fillOpacity="0.04"
/>
<Polygon
points={`
${winDim.width},0
${winDim.width - 100},0
0,${winDim.height - 60}
0,${winDim.height}
${winDim.width},${winDim.height}`}
fill="#fff"
fillOpacity="0.04"
/>
<Polygon
points={`
${winDim.width},100
0,${winDim.height}
${winDim.width},${winDim.height}`}
fill="#fff"
fillOpacity="0.04"
/>
</Svg>*/
}
<SafeAreaView style={styles.innerContainer}>
<Login />
</SafeAreaView>