Add account creation

zio/stable
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

@ -291,8 +291,6 @@ PODS:
- React-jsi (= 0.68.2)
- React-logger (= 0.68.2)
- React-perflogger (= 0.68.2)
- rn-fetch-blob (0.12.0):
- React-Core
- RNCAsyncStorage (1.17.10):
- React-Core
- RNCClipboard (1.11.1):
@ -370,7 +368,6 @@ DEPENDENCIES:
- React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
- React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- rn-fetch-blob (from `../node_modules/rn-fetch-blob`)
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
- "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)"
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
@ -449,8 +446,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/runtimeexecutor"
ReactCommon:
:path: "../node_modules/react-native/ReactCommon"
rn-fetch-blob:
:path: "../node_modules/rn-fetch-blob"
RNCAsyncStorage:
:path: "../node_modules/@react-native-async-storage/async-storage"
RNCClipboard:
@ -502,7 +497,6 @@ SPEC CHECKSUMS:
React-RCTVibration: 79040b92bfa9c3c2d2cb4f57e981164ec7ab9374
React-runtimeexecutor: b960b687d2dfef0d3761fbb187e01812ebab8b23
ReactCommon: 095366164a276d91ea704ce53cb03825c487a3f2
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
RNCAsyncStorage: 0c357f3156fcb16c8589ede67cc036330b6698ca
RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd
RNGestureHandler: 28ad20bf02257791f7f137b31beef34b9549f54b

View File

@ -21,6 +21,7 @@
"@react-native-clipboard/clipboard": "^1.10.0",
"@zxing/text-encoding": "^0.9.0",
"base64-js": "^1.5.1",
"email-validator": "^2.0.4",
"lodash.omit": "^4.5.0",
"mobx": "^6.6.1",
"mobx-react-lite": "^3.4.0",

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,33 +38,53 @@ export class SessionModel {
}
serialize(): unknown {
return this.data
return {
data: this.data,
onboardingState: this.onboardingState,
}
}
hydrate(v: unknown) {
if (isObj(v)) {
if (hasProp(v, 'data') && isObj(v.data)) {
const data: SessionData = {
service: '',
token: '',
username: '',
userdid: '',
}
if (hasProp(v, 'service') && typeof v.service === 'string') {
data.service = v.service
if (hasProp(v.data, 'service') && typeof v.data.service === 'string') {
data.service = v.data.service
}
if (hasProp(v, 'token') && typeof v.token === 'string') {
data.token = v.token
if (hasProp(v.data, 'token') && typeof v.data.token === 'string') {
data.token = v.data.token
}
if (hasProp(v, 'username') && typeof v.username === 'string') {
data.username = v.username
if (
hasProp(v.data, 'username') &&
typeof v.data.username === 'string'
) {
data.username = v.data.username
}
if (hasProp(v, 'userdid') && typeof v.userdid === 'string') {
data.userdid = v.userdid
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 (
this.data &&
hasProp(v, 'onboardingState') &&
isObj(v.onboardingState)
) {
if (
hasProp(v.onboardingState, 'stage') &&
typeof v.onboardingState === 'string'
) {
this.onboardingState = v.onboardingState
}
}
}
}
clear() {
@ -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,25 +11,21 @@ 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 SigninOrCreateAccount = ({
onPressSignin,
}: {
onPressSignin: () => void
}) => {
const winDim = useWindowDimensions()
const halfWidth = winDim.width / 2
const Logo = () => {
return (
<>
<View style={styles.hero}>
<View style={styles.logo}>
<Svg width="100" height="100">
<Circle
@ -40,38 +36,10 @@ const SigninOrCreateAccount = ({
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"
/>
<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"
@ -85,11 +53,27 @@ const SigninOrCreateAccount = ({
</SvgText>
</Svg>
</View>
)
}
const SigninOrCreateAccount = ({
onPressSignin,
onPressCreateAccount,
}: {
onPressSignin: () => void
onPressCreateAccount: () => void
}) => {
const winDim = useWindowDimensions()
const halfWidth = winDim.width / 2
return (
<>
<View style={styles.hero}>
<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,20 +139,29 @@ 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>
<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>
) : undefined}
<View style={styles.groupContent}>
<View style={[s.mb5]}>
<FontAwesomeIcon icon="envelope" style={styles.groupContentIcon} />
<TextInput
style={styles.textInput}
placeholder="Email or username"
placeholderTextColor={colors.blue0}
autoCapitalize="none"
autoFocus
value={username}
@ -176,10 +169,12 @@ const Signin = ({onPressBack}: {onPressBack: () => void}) => {
editable={!isProcessing}
/>
</View>
<View style={[s.mb5]}>
<View style={styles.groupContent}>
<FontAwesomeIcon icon="lock" style={styles.groupContentIcon} />
<TextInput
style={styles.textInput}
placeholder="Password"
placeholderTextColor={colors.blue0}
autoCapitalize="none"
secureTextEntry
value={password}
@ -187,25 +182,10 @@ 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>
<Text style={[s.white, s.f18, s.pl5]}>Back</Text>
</TouchableOpacity>
<View style={s.flex1} />
<TouchableOpacity onPress={onPressNext}>
@ -216,25 +196,259 @@ const Signin = ({onPressBack}: {onPressBack: () => void}) => {
)}
</TouchableOpacity>
</View>
</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 address"
placeholderTextColor={colors.blue0}
autoCapitalize="none"
value={email}
onChangeText={setEmail}
editable={!isProcessing}
/>
</View>
<View style={styles.groupContent}>
<FontAwesomeIcon icon="lock" style={styles.groupContentIcon} />
<TextInput
style={[styles.textInput]}
placeholder="Choose your password"
placeholderTextColor={colors.blue0}
autoCapitalize="none"
secureTextEntry
value={password}
onChangeText={setPassword}
editable={!isProcessing}
/>
</View>
</View>
<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>
)}
<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>

View File

@ -2195,54 +2195,6 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
"@stablelib/binary@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@stablelib/binary/-/binary-1.0.1.tgz#c5900b94368baf00f811da5bdb1610963dfddf7f"
integrity sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q==
dependencies:
"@stablelib/int" "^1.0.1"
"@stablelib/ed25519@^1.0.2":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@stablelib/ed25519/-/ed25519-1.0.3.tgz#f8fdeb6f77114897c887bb6a3138d659d3f35996"
integrity sha512-puIMWaX9QlRsbhxfDc5i+mNPMY+0TmQEskunY1rZEBPi1acBCVQAhnsk/1Hk50DGPtVsZtAWQg4NHGlVaO9Hqg==
dependencies:
"@stablelib/random" "^1.0.2"
"@stablelib/sha512" "^1.0.1"
"@stablelib/wipe" "^1.0.1"
"@stablelib/hash@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@stablelib/hash/-/hash-1.0.1.tgz#3c944403ff2239fad8ebb9015e33e98444058bc5"
integrity sha512-eTPJc/stDkdtOcrNMZ6mcMK1e6yBbqRBaNW55XA1jU8w/7QdnCF0CmMmOD1m7VSkBR44PWrMHU2l6r8YEQHMgg==
"@stablelib/int@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@stablelib/int/-/int-1.0.1.tgz#75928cc25d59d73d75ae361f02128588c15fd008"
integrity sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==
"@stablelib/random@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@stablelib/random/-/random-1.0.2.tgz#2dece393636489bf7e19c51229dd7900eddf742c"
integrity sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w==
dependencies:
"@stablelib/binary" "^1.0.1"
"@stablelib/wipe" "^1.0.1"
"@stablelib/sha512@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@stablelib/sha512/-/sha512-1.0.1.tgz#6da700c901c2c0ceacbd3ae122a38ac57c72145f"
integrity sha512-13gl/iawHV9zvDKciLo1fQ8Bgn2Pvf7OV6amaRVKiq3pjQ3UmEpXxWiAfV8tYjUpeZroBxtyrwtdooQT/i3hzw==
dependencies:
"@stablelib/binary" "^1.0.1"
"@stablelib/hash" "^1.0.1"
"@stablelib/wipe" "^1.0.1"
"@stablelib/wipe@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@stablelib/wipe/-/wipe-1.0.1.tgz#d21401f1d59ade56a62e139462a97f104ed19a36"
integrity sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==
"@surma/rollup-plugin-off-main-thread@^2.2.3":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
@ -4947,6 +4899,11 @@ electron-to-chromium@^1.4.251:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.257.tgz#895dc73c6bb58d1235dc80879ecbca0bcba96e2c"
integrity sha512-C65sIwHqNnPC2ADMfse/jWTtmhZMII+x6ADI9gENzrOiI7BpxmfKFE84WkIEl5wEg+7+SfIkwChDlsd1Erju2A==
email-validator@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-2.0.4.tgz#b8dfaa5d0dae28f1b03c95881d904d4e40bfe7ed"
integrity sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==
emittery@^0.10.2:
version "0.10.2"
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.10.2.tgz#902eec8aedb8c41938c46e9385e9db7e03182933"
@ -8722,11 +8679,6 @@ multicast-dns@^7.2.5:
dns-packet "^5.2.2"
thunky "^1.0.2"
multiformats@^9.4.2:
version "9.9.0"
resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37"
integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==
nanoid@^3.3.1, nanoid@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
@ -9037,11 +8989,6 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
dependencies:
wrappy "1"
one-webcrypto@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/one-webcrypto/-/one-webcrypto-1.0.3.tgz#f951243cde29b79b6745ad14966fc598a609997c"
integrity sha512-fu9ywBVBPx0gS9K0etIROTiCkvI5S1TDjFsYFb3rC1ewFxeOqsbzq7aIMBHsYfrTHBcGXJaONXXjTl8B01cW1Q==
onetime@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
@ -10930,7 +10877,7 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.2.1, semver@^7.3.2, semver@^7.3.5, semver@^7.3.6, semver@^7.3.7:
semver@^7.2.1, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7:
version "7.3.7"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
@ -11904,16 +11851,6 @@ ua-parser-js@^0.7.30:
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6"
integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==
ucans@0.9.1:
version "0.9.1"
resolved "https://registry.yarnpkg.com/ucans/-/ucans-0.9.1.tgz#d4ed0ed61d11ef13925512d365b26c5c9451b852"
integrity sha512-Vr2z5cy3YcPDhK9RY5VOfoqrXEml3GmZCGovFhLOIVji5PPiR/pkA2ME9jGWqLBQ1mj3494aBjxcMu4DreaAcg==
dependencies:
"@stablelib/ed25519" "^1.0.2"
one-webcrypto "^1.0.3"
semver "^7.3.6"
uint8arrays "^3.0.0"
uglify-es@^3.1.9:
version "3.3.9"
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
@ -11922,13 +11859,6 @@ uglify-es@^3.1.9:
commander "~2.13.0"
source-map "~0.6.1"
uint8arrays@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.1.0.tgz#8186b8eafce68f28bd29bd29d683a311778901e2"
integrity sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog==
dependencies:
multiformats "^9.4.2"
unbox-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"