about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2022-06-10 11:55:09 -0500
committerPaul Frazee <pfrazee@gmail.com>2022-06-10 11:55:09 -0500
commitfaddda83f04b46bcdaab5c225cab696fc7a820cd (patch)
tree8a7cacb04f0a480d90a8e5dc9f9daae00f27c802 /src
parent967f9fc474f2903dd2c12ef4f662ead1592ea26c (diff)
downloadvoidsky-faddda83f04b46bcdaab5c225cab696fc7a820cd.tar.zst
(WIP) Add initial API client
Diffstat (limited to 'src')
-rw-r--r--src/api/index.ts97
-rw-r--r--src/screens/Login.tsx26
-rw-r--r--src/screens/Signup.tsx50
-rw-r--r--src/state/env.ts3
-rw-r--r--src/state/models/session.ts58
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',
   }
 }