about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2022-07-18 15:24:37 -0500
committerPaul Frazee <pfrazee@gmail.com>2022-07-18 15:24:37 -0500
commit1d00f3b9840003601175d5394af34b29222db4e3 (patch)
treef835e910d5f11b84ce2403e307caea080c34ab79
parentde87ec17d1673855fdfe2ccd125d734591969dd4 (diff)
downloadvoidsky-1d00f3b9840003601175d5394af34b29222db4e3.tar.zst
Add mock API and reorg code for clarity
-rw-r--r--package.json2
-rw-r--r--scripts/testing-server.mjs17
-rw-r--r--src/App.native.tsx3
-rw-r--r--src/App.web.tsx2
-rw-r--r--src/api/index.ts118
-rw-r--r--src/platform/auth-flow.native.ts2
-rw-r--r--src/platform/auth-flow.ts2
-rw-r--r--src/state/env.ts223
-rw-r--r--src/state/index.ts26
-rw-r--r--src/state/lib/auth.ts (renamed from src/state/auth.ts)8
-rw-r--r--src/state/lib/storage.ts (renamed from src/state/storage.ts)0
-rw-r--r--src/state/models/me.ts48
-rw-r--r--src/state/models/root-store.ts3
-rw-r--r--src/state/models/session.ts14
-rw-r--r--src/view/routes/index.tsx (renamed from src/routes/index.tsx)4
-rw-r--r--src/view/routes/types.ts (renamed from src/routes/types.ts)0
-rw-r--r--src/view/screens/Home.tsx (renamed from src/screens/Home.tsx)4
-rw-r--r--src/view/screens/Login.tsx (renamed from src/screens/Login.tsx)4
-rw-r--r--src/view/screens/Menu.tsx (renamed from src/screens/Menu.tsx)2
-rw-r--r--src/view/screens/NotFound.tsx (renamed from src/screens/NotFound.tsx)2
-rw-r--r--src/view/screens/Notifications.tsx (renamed from src/screens/Notifications.tsx)2
-rw-r--r--src/view/screens/Profile.tsx (renamed from src/screens/Profile.tsx)2
-rw-r--r--src/view/screens/Search.tsx (renamed from src/screens/Search.tsx)2
-rw-r--r--src/view/screens/Signup.tsx (renamed from src/screens/Signup.tsx)4
-rw-r--r--src/view/shell/desktop-web/left-column.tsx (renamed from src/platform/desktop-web/left-column.tsx)0
-rw-r--r--src/view/shell/desktop-web/right-column.tsx (renamed from src/platform/desktop-web/right-column.tsx)0
-rw-r--r--src/view/shell/desktop-web/shell.tsx (renamed from src/platform/desktop-web/shell.tsx)2
-rw-r--r--src/view/shell/index.tsx (renamed from src/platform/shell.tsx)2
-rw-r--r--yarn.lock28
29 files changed, 357 insertions, 169 deletions
diff --git a/package.json b/package.json
index de09b8214..f45a3e542 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
   "dependencies": {
     "@adxp/auth": "*",
     "@adxp/common": "*",
+    "@adxp/mock-api": "git+ssh://git@github.com:bluesky-social/adx-mock-api.git#dc669c19d46b6b98dd692f493276303667670502",
     "@react-native-async-storage/async-storage": "^1.17.6",
     "@react-navigation/bottom-tabs": "^6.3.1",
     "@react-navigation/native": "^6.0.10",
@@ -32,6 +33,7 @@
     "react-native-inappbrowser-reborn": "^3.6.3",
     "react-native-safe-area-context": "^4.3.1",
     "react-native-screens": "^3.13.1",
+    "react-native-url-polyfill": "^1.3.0",
     "react-native-web": "^0.17.7",
     "ucans": "0.9.1"
   },
diff --git a/scripts/testing-server.mjs b/scripts/testing-server.mjs
index adc214cf9..f3c68e535 100644
--- a/scripts/testing-server.mjs
+++ b/scripts/testing-server.mjs
@@ -2,8 +2,11 @@ import {IpldStore} from '@adxp/common'
 import PDSServer from '@adxp/server/dist/server.js'
 import PDSDatabase from '@adxp/server/dist/db/index.js'
 import WSRelayServer from '@adxp/ws-relay/dist/index.js'
+import AuthLobbyServer from '@adxp/auth-lobby'
 
 const PDS_PORT = 2583
+const AUTH_LOBBY1_PORT = 3001
+const AUTH_LOBBY2_PORT = 3002
 const WSR_PORT = 3005
 
 async function start() {
@@ -15,11 +18,19 @@ async function start() {
   await db.createTables()
   PDSServer(serverBlockstore, db, PDS_PORT)
 
+  init(AuthLobbyServer, AUTH_LOBBY1_PORT, 'Auth lobby')
+
   if (process.argv.includes('--relay')) {
-    WSRelayServer(WSR_PORT)
-    console.log(`🔁 Relay server running on port ${WSR_PORT}`)
+    init(AuthLobbyServer, AUTH_LOBBY2_PORT, 'Auth lobby 2')
+    init(WSRelayServer, WSR_PORT, 'Relay server')
   } else {
-    console.log('Include --relay to start the WS Relay')
+    console.log('Include --relay to start the WS Relay and second auth lobby')
   }
 }
 start()
+
+function init(fn, port, name) {
+  const s = fn(port)
+  s.on('listening', () => console.log(`✔ ${name} running on port ${port}`))
+  s.on('error', e => console.log(`${name} failed to start:`, e))
+}
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 511a8401a..1326b184b 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -1,7 +1,8 @@
+import 'react-native-url-polyfill/auto'
 import React, {useState, useEffect} from 'react'
 import {whenWebCrypto} from './platform/polyfills.native'
 import {RootStore, setupState, RootStoreProvider} from './state'
-import * as Routes from './routes'
+import * as Routes from './view/routes'
 
 function App() {
   const [rootStore, setRootStore] = useState<RootStore | undefined>(undefined)
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 2fadf993f..34b6ac6cb 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -1,6 +1,6 @@
 import React, {useState, useEffect} from 'react'
 import {RootStore, setupState, RootStoreProvider} from './state'
-import * as Routes from './routes'
+import * as Routes from './view/routes'
 
 function App() {
   const [rootStore, setRootStore] = useState<RootStore | undefined>(undefined)
diff --git a/src/api/index.ts b/src/api/index.ts
deleted file mode 100644
index 6f0dc0b38..000000000
--- a/src/api/index.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-// import {MicroblogDelegator, MicroblogReader, auth} from '@adx/common'
-// import * as ucan from 'ucans'
-
-class MicroblogReader {
-  constructor(public url: string, public did: any) {}
-}
-class MicroblogDelegator {
-  constructor(
-    public url: string,
-    public did: any,
-    public keypair: any,
-    public ucanStore: any,
-  ) {}
-}
-const auth = {
-  async claimFull(_one: any, _two: any) {
-    return {
-      encoded() {
-        return 'todo'
-      },
-    }
-  },
-}
-
-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?: any //ucan.EdKeypair
-  rootAuthToken?: string
-  ucanStore?: any //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 = true //await ucan.EdKeypair.create()
-    cfg.did = cfg.keypair.did()
-    cfg.rootAuthToken = (await auth.claimFull(cfg.did, cfg.keypair)).encoded()
-    cfg.ucanStore = true // 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 = true // ucan.EdKeypair.fromSecretKey(state.secretKeyStr)
-      this.did = this.keypair.did()
-      this.rootAuthToken = state.rootAuthToken
-      this.ucanStore = true // await ucan.Store.fromTokens([this.rootAuthToken])
-    }
-  }
-}
diff --git a/src/platform/auth-flow.native.ts b/src/platform/auth-flow.native.ts
index 596632f17..3c9bd09eb 100644
--- a/src/platform/auth-flow.native.ts
+++ b/src/platform/auth-flow.native.ts
@@ -4,7 +4,7 @@ import * as ucan from 'ucans'
 import {InAppBrowser} from 'react-native-inappbrowser-reborn'
 import {isWeb} from '../platform/detection'
 import {extractHashFragment, makeAppUrl} from '../platform/urls'
-import {ReactNativeStore, parseUrlForUcan} from '../state/auth'
+import {ReactNativeStore, parseUrlForUcan} from '../state/lib/auth'
 import * as env from '../env'
 
 export async function requestAppUcan(
diff --git a/src/platform/auth-flow.ts b/src/platform/auth-flow.ts
index b96fc58e9..fbc85a373 100644
--- a/src/platform/auth-flow.ts
+++ b/src/platform/auth-flow.ts
@@ -1,7 +1,7 @@
 import * as auth from '@adxp/auth'
 import * as ucan from 'ucans'
 import {makeAppUrl} from '../platform/urls'
-import {ReactNativeStore} from '../state/auth'
+import {ReactNativeStore} from '../state/lib/auth'
 import * as env from '../env'
 
 export async function requestAppUcan(
diff --git a/src/state/env.ts b/src/state/env.ts
index c1e11ebd0..0ee59788c 100644
--- a/src/state/env.ts
+++ b/src/state/env.ts
@@ -4,22 +4,35 @@
  */
 
 import {getEnv, IStateTreeNode} from 'mobx-state-tree'
-import {ReactNativeStore} from './auth'
-import {API} from '../api'
+// import {ReactNativeStore} from './auth'
+import {AdxClient, blueskywebSchemas, AdxRepoClient} from '@adxp/mock-api'
+import * as storage from './lib/storage'
+
+export const adx = new AdxClient({
+  pds: 'http://localhost',
+  schemas: blueskywebSchemas,
+})
 
 export class Environment {
-  api = new API()
-  authStore?: ReactNativeStore
+  adx = adx
+  // authStore?: ReactNativeStore
 
   constructor() {}
 
   async setup() {
-    this.authStore = await ReactNativeStore.load()
+    await adx.setupMock(
+      () => storage.load('mock-root'),
+      async root => {
+        await storage.save('mock-root', root)
+      },
+      generateMockData,
+    )
+    // this.authStore = await ReactNativeStore.load()
   }
 }
 
 /**
- * Extension to the MST models that adds the environment property.
+ * Extension to the MST models that adds the env property.
  * Usage:
  *
  *   .extend(withEnvironment)
@@ -27,8 +40,204 @@ export class Environment {
  */
 export const withEnvironment = (self: IStateTreeNode) => ({
   views: {
-    get environment() {
+    get env() {
       return getEnv<Environment>(self)
     },
   },
 })
+
+// TEMPORARY
+// mock api config
+// =======
+
+function* dateGen() {
+  let start = 1657846031914
+  while (true) {
+    yield new Date(start).toISOString()
+    start += 1e3
+  }
+}
+const date = dateGen()
+
+function repo(didOrName: string) {
+  const userDb = adx.mockDb.getUser(didOrName)
+  if (!userDb) throw new Error(`User not found: ${didOrName}`)
+  return adx.mainPds.repo(userDb.did, userDb.writable)
+}
+
+export async function generateMockData() {
+  await adx.mockDb.addUser({name: 'alice.com', writable: true})
+  await adx.mockDb.addUser({name: 'bob.com', writable: true})
+  await adx.mockDb.addUser({name: 'carla.com', writable: true})
+
+  const alice = repo('alice.com')
+  const bob = repo('bob.com')
+  const carla = repo('carla.com')
+
+  await alice.collection('blueskyweb.xyz:Profiles').put('Profile', 'profile', {
+    $type: 'blueskyweb.xyz:Profile',
+    displayName: 'Alice',
+    description: 'Test user 1',
+  })
+  await bob.collection('blueskyweb.xyz:Profiles').put('Profile', 'profile', {
+    $type: 'blueskyweb.xyz:Profile',
+    displayName: 'Bob',
+    description: 'Test user 2',
+  })
+  await carla.collection('blueskyweb.xyz:Profiles').put('Profile', 'profile', {
+    $type: 'blueskyweb.xyz:Profile',
+    displayName: 'Carla',
+    description: 'Test user 3',
+  })
+
+  // everybody follows everybody
+  const follow = async (who: AdxRepoClient, subjectName: string) => {
+    const subjectDb = adx.mockDb.getUser(subjectName)
+    return who.collection('blueskyweb.xyz:Follows').create('Follow', {
+      $type: 'blueskyweb.xyz:Follow',
+      subject: {
+        did: subjectDb?.did,
+        name: subjectDb?.name,
+      },
+      createdAt: date.next().value,
+    })
+  }
+  await follow(alice, 'bob.com')
+  await follow(alice, 'carla.com')
+  await follow(bob, 'alice.com')
+  await follow(bob, 'carla.com')
+  await follow(carla, 'alice.com')
+  await follow(carla, 'bob.com')
+
+  // 2 posts on each user
+  const alicePosts: {uri: string}[] = []
+  for (let i = 0; i < 2; i++) {
+    alicePosts.push(
+      await alice.collection('blueskyweb.xyz:Posts').create('Post', {
+        $type: 'blueskyweb.xyz:Post',
+        text: `Alice post ${i + 1}`,
+        createdAt: date.next().value,
+      }),
+    )
+    await bob.collection('blueskyweb.xyz:Posts').create('Post', {
+      $type: 'blueskyweb.xyz:Post',
+      text: `Bob post ${i + 1}`,
+      createdAt: date.next().value,
+    })
+    await carla.collection('blueskyweb.xyz:Posts').create('Post', {
+      $type: 'blueskyweb.xyz:Post',
+      text: `Carla post ${i + 1}`,
+      createdAt: date.next().value,
+    })
+  }
+
+  // small thread of replies on alice's first post
+  const bobReply1 = await bob
+    .collection('blueskyweb.xyz:Posts')
+    .create('Post', {
+      $type: 'blueskyweb.xyz:Post',
+      text: 'Bob reply',
+      reply: {root: alicePosts[0].uri, parent: alicePosts[0].uri},
+      createdAt: date.next().value,
+    })
+  const carlaReply1 = await carla
+    .collection('blueskyweb.xyz:Posts')
+    .create('Post', {
+      $type: 'blueskyweb.xyz:Post',
+      text: 'Carla reply',
+      reply: {root: alicePosts[0].uri, parent: alicePosts[0].uri},
+      createdAt: date.next().value,
+    })
+  const aliceReply1 = await alice
+    .collection('blueskyweb.xyz:Posts')
+    .create('Post', {
+      $type: 'blueskyweb.xyz:Post',
+      text: 'Alice reply',
+      reply: {root: alicePosts[0].uri, parent: bobReply1.uri},
+      createdAt: date.next().value,
+    })
+
+  // bob and carla repost alice's first post
+  await bob.collection('blueskyweb.xyz:Posts').create('Repost', {
+    $type: 'blueskyweb.xyz:Repost',
+    subject: alicePosts[0].uri,
+    createdAt: date.next().value,
+  })
+  await carla.collection('blueskyweb.xyz:Posts').create('Repost', {
+    $type: 'blueskyweb.xyz:Repost',
+    subject: alicePosts[0].uri,
+    createdAt: date.next().value,
+  })
+
+  // bob likes all of alice's posts
+  for (let i = 0; i < 2; i++) {
+    await bob.collection('blueskyweb.xyz:Likes').create('Like', {
+      $type: 'blueskyweb.xyz:Like',
+      subject: alicePosts[i].uri,
+      createdAt: date.next().value,
+    })
+  }
+
+  // carla likes all of alice's posts and everybody's replies
+  for (let i = 0; i < 2; i++) {
+    await carla.collection('blueskyweb.xyz:Likes').create('Like', {
+      $type: 'blueskyweb.xyz:Like',
+      subject: alicePosts[i].uri,
+      createdAt: date.next().value,
+    })
+  }
+  await carla.collection('blueskyweb.xyz:Likes').create('Like', {
+    $type: 'blueskyweb.xyz:Like',
+    subject: aliceReply1.uri,
+    createdAt: date.next().value,
+  })
+  await carla.collection('blueskyweb.xyz:Likes').create('Like', {
+    $type: 'blueskyweb.xyz:Like',
+    subject: bobReply1.uri,
+    createdAt: date.next().value,
+  })
+
+  // give alice 3 badges, 2 from bob and 2 from carla, with one ignored
+  const inviteBadge = await bob
+    .collection('blueskyweb.xyz:Badges')
+    .create('Badge', {
+      $type: 'blueskyweb.xyz:Badge',
+      subject: {did: alice.did, name: 'alice.com'},
+      assertion: {type: 'invite'},
+      createdAt: date.next().value,
+    })
+  const techTagBadge1 = await bob
+    .collection('blueskyweb.xyz:Badges')
+    .create('Badge', {
+      $type: 'blueskyweb.xyz:Badge',
+      subject: {did: alice.did, name: 'alice.com'},
+      assertion: {type: 'tag', tag: 'tech'},
+      createdAt: date.next().value,
+    })
+  const techTagBadge2 = await carla
+    .collection('blueskyweb.xyz:Badges')
+    .create('Badge', {
+      $type: 'blueskyweb.xyz:Badge',
+      subject: {did: alice.did, name: 'alice.com'},
+      assertion: {type: 'tag', tag: 'tech'},
+      createdAt: date.next().value,
+    })
+  const employeeBadge = await bob
+    .collection('blueskyweb.xyz:Badges')
+    .create('Badge', {
+      $type: 'blueskyweb.xyz:Badge',
+      subject: {did: alice.did, name: 'alice.com'},
+      assertion: {type: 'employee'},
+      createdAt: date.next().value,
+    })
+  await alice.collection('blueskyweb.xyz:Profiles').put('Profile', 'profile', {
+    $type: 'blueskyweb.xyz:Profile',
+    displayName: 'Alice',
+    description: 'Test user 1',
+    badges: [
+      {uri: inviteBadge.uri},
+      {uri: techTagBadge1.uri},
+      {uri: techTagBadge2.uri},
+    ],
+  })
+}
diff --git a/src/state/index.ts b/src/state/index.ts
index 6040f8f9d..24c3b9430 100644
--- a/src/state/index.ts
+++ b/src/state/index.ts
@@ -5,8 +5,8 @@ import {
   createDefaultRootStore,
 } from './models/root-store'
 import {Environment} from './env'
-import * as storage from './storage'
-import * as auth from './auth'
+import * as storage from './lib/storage'
+// import * as auth from './auth' TODO
 
 const ROOT_STATE_STORAGE_KEY = 'root'
 
@@ -29,15 +29,19 @@ export async function setupState() {
     storage.save(ROOT_STATE_STORAGE_KEY, snapshot),
   )
 
-  if (env.authStore) {
-    const isAuthed = await auth.isAuthed(env.authStore)
-    rootStore.session.setAuthed(isAuthed)
-
-    // handle redirect from auth
-    if (await auth.initialLoadUcanCheck(env.authStore)) {
-      rootStore.session.setAuthed(true)
-    }
-  }
+  // TODO
+  rootStore.session.setAuthed(true)
+  // if (env.authStore) {
+  //   const isAuthed = await auth.isAuthed(env.authStore)
+  //   rootStore.session.setAuthed(isAuthed)
+
+  //   // handle redirect from auth
+  //   if (await auth.initialLoadUcanCheck(env.authStore)) {
+  //     rootStore.session.setAuthed(true)
+  //   }
+  // }
+  await rootStore.me.load()
+  console.log(rootStore.me)
 
   return rootStore
 }
diff --git a/src/state/auth.ts b/src/state/lib/auth.ts
index a8483b926..d758745ed 100644
--- a/src/state/auth.ts
+++ b/src/state/lib/auth.ts
@@ -1,7 +1,11 @@
 import * as auth from '@adxp/auth'
 import * as ucan from 'ucans'
-import {getInitialURL, extractHashFragment, clearHash} from '../platform/urls'
-import * as authFlow from '../platform/auth-flow'
+import {
+  getInitialURL,
+  extractHashFragment,
+  clearHash,
+} from '../../platform/urls'
+import * as authFlow from '../../platform/auth-flow'
 import * as storage from './storage'
 
 const SCOPE = auth.writeCap(
diff --git a/src/state/storage.ts b/src/state/lib/storage.ts
index dc5fb620f..dc5fb620f 100644
--- a/src/state/storage.ts
+++ b/src/state/lib/storage.ts
diff --git a/src/state/models/me.ts b/src/state/models/me.ts
new file mode 100644
index 000000000..bc4b13148
--- /dev/null
+++ b/src/state/models/me.ts
@@ -0,0 +1,48 @@
+import {Instance, SnapshotOut, types, flow, getRoot} from 'mobx-state-tree'
+import {RootStore} from './root-store'
+import {withEnvironment} from '../env'
+
+export const MeModel = types
+  .model('Me')
+  .props({
+    did: types.maybe(types.string),
+    name: types.maybe(types.string),
+    displayName: types.maybe(types.string),
+    description: types.maybe(types.string),
+  })
+  .extend(withEnvironment)
+  .actions(self => ({
+    load: flow(function* () {
+      const sess = (getRoot(self) as RootStore).session
+      if (sess.isAuthed) {
+        // TODO temporary
+        const userDb = self.env.adx.mockDb.mainUser
+        self.did = userDb.did
+        self.name = userDb.name
+        const profile = yield self.env.adx
+          .repo(self.did, true)
+          .collection('blueskyweb.xyz:Profiles')
+          .get('Profile', 'profile')
+          .catch(_ => undefined)
+        if (profile?.valid) {
+          self.displayName = profile.value.displayName
+          self.description = profile.value.description
+        } else {
+          self.displayName = ''
+          self.description = ''
+        }
+      } else {
+        self.did = undefined
+        self.name = undefined
+        self.displayName = undefined
+        self.description = undefined
+      }
+    }),
+  }))
+
+export interface Me extends Instance<typeof MeModel> {}
+export interface MeSnapshot extends SnapshotOut<typeof MeModel> {}
+
+export function createDefaultMe() {
+  return {}
+}
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index 143c59ea1..b38b36e8a 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -5,9 +5,11 @@
 import {Instance, SnapshotOut, types} from 'mobx-state-tree'
 import {createContext, useContext} from 'react'
 import {SessionModel, createDefaultSession} from './session'
+import {MeModel, createDefaultMe} from './me'
 
 export const RootStoreModel = types.model('RootStore').props({
   session: SessionModel,
+  me: MeModel,
 })
 
 export interface RootStore extends Instance<typeof RootStoreModel> {}
@@ -16,6 +18,7 @@ export interface RootStoreSnapshot extends SnapshotOut<typeof RootStoreModel> {}
 export function createDefaultRootStore() {
   return {
     session: createDefaultSession(),
+    me: createDefaultMe(),
   }
 }
 
diff --git a/src/state/models/session.ts b/src/state/models/session.ts
index c032d7594..3b52b8fc6 100644
--- a/src/state/models/session.ts
+++ b/src/state/models/session.ts
@@ -1,6 +1,6 @@
 import {Instance, SnapshotOut, types, flow} from 'mobx-state-tree'
 // import {UserConfig} from '../../api'
-import * as auth from '../auth'
+import * as auth from '../lib/auth'
 import {withEnvironment} from '../env'
 
 export const SessionModel = types
@@ -24,10 +24,10 @@ export const SessionModel = types
       self.uiIsProcessing = true
       self.uiError = undefined
       try {
-        if (!self.environment.authStore) {
+        if (!self.env.authStore) {
           throw new Error('Auth store not initialized')
         }
-        const res = yield auth.requestAppUcan(self.environment.authStore)
+        const res = yield auth.requestAppUcan(self.env.authStore)
         self.isAuthed = res
         self.uiIsProcessing = false
         return res
@@ -42,10 +42,10 @@ export const SessionModel = types
       self.uiIsProcessing = true
       self.uiError = undefined
       try {
-        if (!self.environment.authStore) {
+        if (!self.env.authStore) {
           throw new Error('Auth store not initialized')
         }
-        const res = yield auth.logout(self.environment.authStore)
+        const res = yield auth.logout(self.env.authStore)
         self.isAuthed = false
         self.uiIsProcessing = false
         return res
@@ -65,7 +65,7 @@ export const SessionModel = types
         //   secretKeyStr: self.secretKeyStr,
         //   rootAuthToken: self.rootAuthToken,
         // })
-        // self.environment.api.setUserCfg(cfg)
+        // self.env.api.setUserCfg(cfg)
         self.isAuthed = true
         self.uiIsProcessing = false
         return true
@@ -86,7 +86,7 @@ export const SessionModel = types
         // self.secretKeyStr = state.secretKeyStr
         // self.rootAuthToken = state.rootAuthToken
         self.isAuthed = true
-        // self.environment.api.setUserCfg(cfg)
+        // self.env.api.setUserCfg(cfg)
       } catch (e: any) {
         console.error('Failed to create test account', e)
         self.uiError = e.toString()
diff --git a/src/routes/index.tsx b/src/view/routes/index.tsx
index 32398e9ad..6351dea6a 100644
--- a/src/routes/index.tsx
+++ b/src/view/routes/index.tsx
@@ -10,8 +10,8 @@ import {createNativeStackNavigator} from '@react-navigation/native-stack'
 import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'
 import {observer} from 'mobx-react-lite'
 import type {RootTabsParamList} from './types'
-import {useStores} from '../state'
-import * as platform from '../platform/detection'
+import {useStores} from '../../state'
+import * as platform from '../../platform/detection'
 import {Home} from '../screens/Home'
 import {Search} from '../screens/Search'
 import {Notifications} from '../screens/Notifications'
diff --git a/src/routes/types.ts b/src/view/routes/types.ts
index d92594bbe..d92594bbe 100644
--- a/src/routes/types.ts
+++ b/src/view/routes/types.ts
diff --git a/src/screens/Home.tsx b/src/view/screens/Home.tsx
index ed95121ea..5210d9d40 100644
--- a/src/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -1,8 +1,8 @@
 import React from 'react'
 import {Text, Button, View} from 'react-native'
-import {Shell} from '../platform/shell'
+import {Shell} from '../shell'
 import type {RootTabsScreenProps} from '../routes/types'
-import {useStores} from '../state'
+import {useStores} from '../../state'
 
 export function Home({navigation}: RootTabsScreenProps<'Home'>) {
   const store = useStores()
diff --git a/src/screens/Login.tsx b/src/view/screens/Login.tsx
index 36280e87a..207557369 100644
--- a/src/screens/Login.tsx
+++ b/src/view/screens/Login.tsx
@@ -1,9 +1,9 @@
 import React from 'react'
 import {Text, Button, View, ActivityIndicator} from 'react-native'
 import {observer} from 'mobx-react-lite'
-import {Shell} from '../platform/shell'
+import {Shell} from '../shell'
 import type {RootTabsScreenProps} from '../routes/types'
-import {useStores} from '../state'
+import {useStores} from '../../state'
 
 export const Login = observer(({navigation}: RootTabsScreenProps<'Login'>) => {
   const store = useStores()
diff --git a/src/screens/Menu.tsx b/src/view/screens/Menu.tsx
index 9cdda4f2a..8cf93676e 100644
--- a/src/screens/Menu.tsx
+++ b/src/view/screens/Menu.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {Shell} from '../platform/shell'
+import {Shell} from '../shell'
 import {ScrollView, Text, View} from 'react-native'
 import type {RootTabsScreenProps} from '../routes/types'
 
diff --git a/src/screens/NotFound.tsx b/src/view/screens/NotFound.tsx
index f4d9d510c..3f6dd7aa0 100644
--- a/src/screens/NotFound.tsx
+++ b/src/view/screens/NotFound.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {Shell} from '../platform/shell'
+import {Shell} from '../shell'
 import {Text, Button, View} from 'react-native'
 import type {RootTabsScreenProps} from '../routes/types'
 
diff --git a/src/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index 292f4593f..5bade68fa 100644
--- a/src/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {Shell} from '../platform/shell'
+import {Shell} from '../shell'
 import {Text, View} from 'react-native'
 import type {RootTabsScreenProps} from '../routes/types'
 
diff --git a/src/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 76915b48f..2c93f4bf9 100644
--- a/src/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {Shell} from '../platform/shell'
+import {Shell} from '../shell'
 import {View, Text} from 'react-native'
 import type {RootTabsScreenProps} from '../routes/types'
 
diff --git a/src/screens/Search.tsx b/src/view/screens/Search.tsx
index d456cd196..2f111cf72 100644
--- a/src/screens/Search.tsx
+++ b/src/view/screens/Search.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {Shell} from '../platform/shell'
+import {Shell} from '../shell'
 import {Text, View} from 'react-native'
 import type {RootTabsScreenProps} from '../routes/types'
 
diff --git a/src/screens/Signup.tsx b/src/view/screens/Signup.tsx
index e09ab5dd6..8ca47e3ef 100644
--- a/src/screens/Signup.tsx
+++ b/src/view/screens/Signup.tsx
@@ -1,9 +1,9 @@
 import React from 'react'
 import {Text, Button, View, ActivityIndicator} from 'react-native'
 import {observer} from 'mobx-react-lite'
-import {Shell} from '../platform/shell'
+import {Shell} from '../shell'
 import type {RootTabsScreenProps} from '../routes/types'
-import {useStores} from '../state'
+import {useStores} from '../../state'
 
 export const Signup = observer(
   ({navigation}: RootTabsScreenProps<'Signup'>) => {
diff --git a/src/platform/desktop-web/left-column.tsx b/src/view/shell/desktop-web/left-column.tsx
index 082231ec9..082231ec9 100644
--- a/src/platform/desktop-web/left-column.tsx
+++ b/src/view/shell/desktop-web/left-column.tsx
diff --git a/src/platform/desktop-web/right-column.tsx b/src/view/shell/desktop-web/right-column.tsx
index 5fe65cac8..5fe65cac8 100644
--- a/src/platform/desktop-web/right-column.tsx
+++ b/src/view/shell/desktop-web/right-column.tsx
diff --git a/src/platform/desktop-web/shell.tsx b/src/view/shell/desktop-web/shell.tsx
index ef880306b..13acbbfed 100644
--- a/src/platform/desktop-web/shell.tsx
+++ b/src/view/shell/desktop-web/shell.tsx
@@ -3,7 +3,7 @@ import {observer} from 'mobx-react-lite'
 import {View, StyleSheet} from 'react-native'
 import {DesktopLeftColumn} from './left-column'
 import {DesktopRightColumn} from './right-column'
-import {useStores} from '../../state'
+import {useStores} from '../../../state'
 
 export const DesktopWebShell: React.FC = observer(({children}) => {
   const store = useStores()
diff --git a/src/platform/shell.tsx b/src/view/shell/index.tsx
index ec8d51e1f..db60ed149 100644
--- a/src/platform/shell.tsx
+++ b/src/view/shell/index.tsx
@@ -1,6 +1,6 @@
 import React from 'react'
 import {SafeAreaView} from 'react-native'
-import {isDesktopWeb} from './detection'
+import {isDesktopWeb} from '../../platform/detection'
 import {DesktopWebShell} from './desktop-web/shell'
 
 export const Shell: React.FC = ({children}) => {
diff --git a/yarn.lock b/yarn.lock
index 4de0d1887..1f989b002 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -55,6 +55,14 @@
     ucans "0.9.0-alpha3"
     uint8arrays "^3.0.0"
 
+"@adxp/mock-api@git+ssh://git@github.com:bluesky-social/adx-mock-api.git#dc669c19d46b6b98dd692f493276303667670502":
+  version "0.0.1"
+  resolved "git+ssh://git@github.com:bluesky-social/adx-mock-api.git#dc669c19d46b6b98dd692f493276303667670502"
+  dependencies:
+    ajv "^8.11.0"
+    ajv-formats "^2.1.1"
+    zod "^3.14.2"
+
 "@adxp/server@*":
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/@adxp/server/-/server-0.0.2.tgz#706d248a5e481a4582657c12d919acb4d96106fb"
@@ -3221,7 +3229,7 @@ ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5:
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
-ajv@^8.0.0, ajv@^8.0.1, ajv@^8.6.0, ajv@^8.8.0:
+ajv@^8.0.0, ajv@^8.0.1, ajv@^8.11.0, ajv@^8.6.0, ajv@^8.8.0:
   version "8.11.0"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f"
   integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==
@@ -4028,7 +4036,7 @@ buffer-from@^1.0.0:
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
   integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
 
-buffer@^5.5.0:
+buffer@^5.4.3, buffer@^5.5.0:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
   integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
@@ -11059,6 +11067,13 @@ react-native-screens@^3.13.1:
     react-freeze "^1.0.0"
     warn-once "^0.1.0"
 
+react-native-url-polyfill@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/react-native-url-polyfill/-/react-native-url-polyfill-1.3.0.tgz#c1763de0f2a8c22cc3e959b654c8790622b6ef6a"
+  integrity sha512-w9JfSkvpqqlix9UjDvJjm1EjSt652zVQ6iwCIj1cVVkwXf4jQhQgTNXY6EVTwuAmUjg6BC6k9RHCBynoLFo3IQ==
+  dependencies:
+    whatwg-url-without-unicode "8.0.0-3"
+
 react-native-web@^0.17.7:
   version "0.17.7"
   resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.17.7.tgz#038899dbc94467a0ca0be214b88a30e0c117b176"
@@ -13249,6 +13264,15 @@ whatwg-mimetype@^2.3.0:
   resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
   integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
 
+whatwg-url-without-unicode@8.0.0-3:
+  version "8.0.0-3"
+  resolved "https://registry.yarnpkg.com/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz#ab6df4bf6caaa6c85a59f6e82c026151d4bb376b"
+  integrity sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==
+  dependencies:
+    buffer "^5.4.3"
+    punycode "^2.1.1"
+    webidl-conversions "^5.0.0"
+
 whatwg-url@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"