Add mock API and reorg code for clarity

This commit is contained in:
Paul Frazee 2022-07-18 15:24:37 -05:00
parent de87ec17d1
commit 1d00f3b984
29 changed files with 356 additions and 168 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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])
}
}
}

View file

@ -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(

View file

@ -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(

View file

@ -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},
],
})
}

View file

@ -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)
// 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)
}
}
// // handle redirect from auth
// if (await auth.initialLoadUcanCheck(env.authStore)) {
// rootStore.session.setAuthed(true)
// }
// }
await rootStore.me.load()
console.log(rootStore.me)
return rootStore
}

View file

@ -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(

48
src/state/models/me.ts Normal file
View file

@ -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 {}
}

View file

@ -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(),
}
}

View file

@ -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()

View file

@ -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'

View file

@ -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()

View file

@ -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()

View file

@ -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'

View file

@ -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'

View file

@ -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'

View file

@ -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'

View file

@ -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'

View file

@ -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'>) => {

View file

@ -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()

View file

@ -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}) => {