diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/api/index.ts | 97 | ||||
-rw-r--r-- | src/screens/Login.tsx | 26 | ||||
-rw-r--r-- | src/screens/Signup.tsx | 50 | ||||
-rw-r--r-- | src/state/env.ts | 3 | ||||
-rw-r--r-- | src/state/models/session.ts | 58 |
5 files changed, 209 insertions, 25 deletions
diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 000000000..f83e65411 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,97 @@ +import {MicroblogDelegator, MicroblogReader, auth} from '@adx/common' +import * as ucan from 'ucans' + +export class API { + userCfg?: UserConfig + reader?: MicroblogReader + writer?: MicroblogDelegator + + setUserCfg(cfg: UserConfig) { + this.userCfg = cfg + this.createReader() + this.createWriter() + } + + private createReader() { + if (!this.userCfg?.serverUrl) { + this.reader = undefined + } else { + this.reader = new MicroblogReader( + this.userCfg.serverUrl, + this.userCfg.did, + ) + } + } + + private createWriter() { + if ( + this.userCfg?.serverUrl && + this.userCfg?.did && + this.userCfg?.keypair && + this.userCfg?.ucanStore + ) { + this.writer = new MicroblogDelegator( + this.userCfg.serverUrl, + this.userCfg.did, + this.userCfg.keypair, + this.userCfg.ucanStore, + ) + } else { + this.writer = undefined + } + } +} + +export interface SerializedUserConfig { + serverUrl?: string + secretKeyStr?: string + rootAuthToken?: string +} + +export class UserConfig { + serverUrl?: string + did?: string + keypair?: ucan.EdKeypair + rootAuthToken?: string + ucanStore?: ucan.Store + + get hasWriteCaps() { + return Boolean(this.did && this.keypair && this.ucanStore) + } + + static async createTest(serverUrl: string) { + const cfg = new UserConfig() + cfg.serverUrl = serverUrl + cfg.keypair = await ucan.EdKeypair.create() + cfg.did = cfg.keypair.did() + cfg.rootAuthToken = (await auth.claimFull(cfg.did, cfg.keypair)).encoded() + cfg.ucanStore = await ucan.Store.fromTokens([cfg.rootAuthToken]) + return cfg + } + + static async hydrate(state: SerializedUserConfig) { + const cfg = new UserConfig() + await cfg.hydrate(state) + return cfg + } + + async serialize(): Promise<SerializedUserConfig> { + return { + serverUrl: this.serverUrl, + secretKeyStr: this.keypair + ? await this.keypair.export('base64') + : undefined, + rootAuthToken: this.rootAuthToken, + } + } + + async hydrate(state: SerializedUserConfig) { + this.serverUrl = state.serverUrl + if (state.secretKeyStr && state.rootAuthToken) { + this.keypair = ucan.EdKeypair.fromSecretKey(state.secretKeyStr) + this.did = this.keypair.did() + this.rootAuthToken = state.rootAuthToken + this.ucanStore = await ucan.Store.fromTokens([this.rootAuthToken]) + } + } +} diff --git a/src/screens/Login.tsx b/src/screens/Login.tsx index 0eea085d5..8451eb3c8 100644 --- a/src/screens/Login.tsx +++ b/src/screens/Login.tsx @@ -1,18 +1,34 @@ import React from 'react' -import {Text, Button, View} from 'react-native' +import {Text, Button, View, ActivityIndicator} from 'react-native' +import {observer} from 'mobx-react-lite' import {Shell} from '../platform/shell' import type {RootTabsScreenProps} from '../routes/types' import {useStores} from '../state' -export function Login({navigation}: RootTabsScreenProps<'Login'>) { +export const Login = observer(({navigation}: RootTabsScreenProps<'Login'>) => { const store = useStores() return ( <Shell> <View style={{justifyContent: 'center', alignItems: 'center'}}> <Text style={{fontSize: 20, fontWeight: 'bold'}}>Sign In</Text> - <Button title="Login" onPress={() => store.session.setAuthed(true)} /> - <Button title="Sign Up" onPress={() => navigation.navigate('Signup')} /> + {store.session.uiError ?? <Text>{store.session.uiError}</Text>} + {store.session.uiState === 'idle' ? ( + <> + {store.session.hasAccount ?? ( + <Button + title="Login" + onPress={() => store.session.loadAccount()} + /> + )} + <Button + title="Sign Up" + onPress={() => navigation.navigate('Signup')} + /> + </> + ) : ( + <ActivityIndicator /> + )} </View> </Shell> ) -} +}) diff --git a/src/screens/Signup.tsx b/src/screens/Signup.tsx index bab7abc1c..1d5915d65 100644 --- a/src/screens/Signup.tsx +++ b/src/screens/Signup.tsx @@ -1,24 +1,36 @@ import React from 'react' +import {Text, Button, View, ActivityIndicator} from 'react-native' +import {observer} from 'mobx-react-lite' import {Shell} from '../platform/shell' -import {Text, Button, View} from 'react-native' import type {RootTabsScreenProps} from '../routes/types' import {useStores} from '../state' -export function Signup({navigation}: RootTabsScreenProps<'Signup'>) { - const store = useStores() - return ( - <Shell> - <View style={{justifyContent: 'center', alignItems: 'center'}}> - <Text style={{fontSize: 20, fontWeight: 'bold'}}>Create Account</Text> - <Button - title="Create new account" - onPress={() => store.session.setAuthed(true)} - /> - <Button - title="Log in to an existing account" - onPress={() => navigation.navigate('Login')} - /> - </View> - </Shell> - ) -} +export const Signup = observer( + ({navigation}: RootTabsScreenProps<'Signup'>) => { + const store = useStores() + return ( + <Shell> + <View style={{justifyContent: 'center', alignItems: 'center'}}> + <Text style={{fontSize: 20, fontWeight: 'bold'}}>Create Account</Text> + {store.session.uiError ?? <Text>{store.session.uiError}</Text>} + {store.session.uiState === 'idle' ? ( + <> + <Button + title="Create new account" + onPress={() => + store.session.createTestAccount('http://localhost:1986') + } + /> + <Button + title="Log in to an existing account" + onPress={() => navigation.navigate('Login')} + /> + </> + ) : ( + <ActivityIndicator /> + )} + </View> + </Shell> + ) + }, +) diff --git a/src/state/env.ts b/src/state/env.ts index 90a2cab5e..59c0554a2 100644 --- a/src/state/env.ts +++ b/src/state/env.ts @@ -4,8 +4,11 @@ */ import {getEnv, IStateTreeNode} from 'mobx-state-tree' +import {API} from '../api' export class Environment { + api = new API() + constructor() {} async setup() {} diff --git a/src/state/models/session.ts b/src/state/models/session.ts index 675feb8bc..a550e2174 100644 --- a/src/state/models/session.ts +++ b/src/state/models/session.ts @@ -1,14 +1,69 @@ -import {Instance, SnapshotOut, types} from 'mobx-state-tree' +import {Instance, SnapshotOut, types, flow} from 'mobx-state-tree' +import {UserConfig} from '../../api' +import {withEnvironment} from '../env' export const SessionModel = types .model('Session') .props({ isAuthed: types.boolean, + uiState: types.enumeration('idle', ['idle', 'working']), + uiError: types.maybe(types.string), + + // TODO: these should be stored somewhere secret + serverUrl: types.maybe(types.string), + secretKeyStr: types.maybe(types.string), + rootAuthToken: types.maybe(types.string), }) + .views(self => ({ + get hasAccount() { + return self.serverUrl && self.secretKeyStr && self.rootAuthToken + }, + })) + .extend(withEnvironment) .actions(self => ({ setAuthed: (v: boolean) => { self.isAuthed = v }, + loadAccount: flow(function* () { + if (!self.hasAccount) { + return false + } + self.uiState = 'working' + self.uiError = undefined + try { + const cfg = yield UserConfig.hydrate({ + serverUrl: self.serverUrl, + secretKeyStr: self.secretKeyStr, + rootAuthToken: self.rootAuthToken, + }) + self.environment.api.setUserCfg(cfg) + self.isAuthed = true + self.uiState = 'idle' + return true + } catch (e: any) { + console.error('Failed to create test account', e) + self.uiError = e.toString() + self.uiState = 'idle' + return false + } + }), + createTestAccount: flow(function* (serverUrl: string) { + self.uiState = 'working' + self.uiError = undefined + try { + const cfg = yield UserConfig.createTest(serverUrl) + const state = yield cfg.serialize() + self.serverUrl = state.serverUrl + self.secretKeyStr = state.secretKeyStr + self.rootAuthToken = state.rootAuthToken + self.isAuthed = true + self.environment.api.setUserCfg(cfg) + } catch (e: any) { + console.error('Failed to create test account', e) + self.uiError = e.toString() + } + self.uiState = 'idle' + }), })) export interface Session extends Instance<typeof SessionModel> {} @@ -17,5 +72,6 @@ export interface SessionSnapshot extends SnapshotOut<typeof SessionModel> {} export function createDefaultSession() { return { isAuthed: false, + uiState: 'idle', } } |