Add base auth & ucan request flow (web only)
parent
09b78a4634
commit
cef133031e
20
README.md
20
README.md
|
@ -14,20 +14,16 @@ Uses:
|
||||||
|
|
||||||
- Setup your environment [using the react native instructions](https://reactnative.dev/docs/environment-setup).
|
- Setup your environment [using the react native instructions](https://reactnative.dev/docs/environment-setup).
|
||||||
- After initial setup:
|
- After initial setup:
|
||||||
- `cd ios ; pod install` Installs the React Navigation deps ([info](https://reactnative.dev/docs/navigation#installation-and-setup)).
|
- `cd ios ; pod install`
|
||||||
- To run the iOS simulator: `yarn ios`
|
- Start the dev servers
|
||||||
- To run the Android simulator: `yarn android`
|
- `yarn dev-pds`
|
||||||
- To run the Web app: `yarn web`
|
- `yarn dev-wallet`
|
||||||
|
- Run the dev app
|
||||||
|
- iOS: `yarn ios`
|
||||||
|
- Android: `yarn android`
|
||||||
|
- Web: `yarn web`
|
||||||
- Tips
|
- Tips
|
||||||
- `npx react-native info` Checks what has been installed.
|
- `npx react-native info` Checks what has been installed.
|
||||||
- Android instructions are a *little* inaccurate but not as much as you might think. I had to manually create a virtual device, then run `yarn android` twice (once to start the emulator and the second time to connect to it).
|
|
||||||
|
|
||||||
## TODOs
|
|
||||||
|
|
||||||
- API
|
|
||||||
- Create mock api
|
|
||||||
- Tests
|
|
||||||
- Should just try to catch errors on basic load
|
|
||||||
|
|
||||||
## Various notes
|
## Various notes
|
||||||
|
|
||||||
|
|
10
package.json
10
package.json
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "app",
|
"name": "pubsq",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -7,11 +7,14 @@
|
||||||
"ios": "react-native run-ios",
|
"ios": "react-native run-ios",
|
||||||
"web": "react-scripts start",
|
"web": "react-scripts start",
|
||||||
"start": "react-native start",
|
"start": "react-native start",
|
||||||
"dev-backend": "node ./scripts/testing-server.mjs",
|
"dev-pds": "node ./scripts/testing-server.mjs",
|
||||||
|
"dev-wallet": "cd node_modules/\\@adxp/auth-lobby && npm run start:authed",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
|
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@adxp/auth": "*",
|
||||||
|
"@adxp/common": "*",
|
||||||
"@react-native-async-storage/async-storage": "^1.17.6",
|
"@react-native-async-storage/async-storage": "^1.17.6",
|
||||||
"@react-navigation/bottom-tabs": "^6.3.1",
|
"@react-navigation/bottom-tabs": "^6.3.1",
|
||||||
"@react-navigation/native": "^6.0.10",
|
"@react-navigation/native": "^6.0.10",
|
||||||
|
@ -29,6 +32,9 @@
|
||||||
"ucans": "0.9.0-alpha3"
|
"ucans": "0.9.0-alpha3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@adxp/auth-lobby": "*",
|
||||||
|
"@adxp/server": "*",
|
||||||
|
"@adxp/ws-relay": "*",
|
||||||
"@babel/core": "^7.12.9",
|
"@babel/core": "^7.12.9",
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@react-native-community/eslint-config": "^2.0.0",
|
"@react-native-community/eslint-config": "^2.0.0",
|
||||||
|
|
|
@ -1,16 +1,25 @@
|
||||||
import {IpldStore} from '@adx/common'
|
import {IpldStore} from '@adxp/common'
|
||||||
import server from '@adx/server/dist/server.js'
|
import PDSServer from '@adxp/server/dist/server.js'
|
||||||
import Database from '@adx/server/dist/db/index.js'
|
import PDSDatabase from '@adxp/server/dist/db/index.js'
|
||||||
|
import WSRelayServer from '@adxp/ws-relay/dist/index.js'
|
||||||
|
|
||||||
const PORT = 1986
|
const PDS_PORT = 2583
|
||||||
|
const WSR_PORT = 3005
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
console.log('Initializing...')
|
console.log('Initializing...')
|
||||||
|
|
||||||
const db = Database.memory()
|
const db = PDSDatabase.memory()
|
||||||
const serverBlockstore = IpldStore.createInMemory()
|
const serverBlockstore = IpldStore.createInMemory()
|
||||||
await db.dropTables()
|
await db.dropTables()
|
||||||
await db.createTables()
|
await db.createTables()
|
||||||
server(serverBlockstore, db, PORT)
|
PDSServer(serverBlockstore, db, PDS_PORT)
|
||||||
|
|
||||||
|
if (process.argv.includes('--relay')) {
|
||||||
|
WSRelayServer(WSR_PORT)
|
||||||
|
console.log(`🔁 Relay server running on port ${WSR_PORT}`)
|
||||||
|
} else {
|
||||||
|
console.log('Include --relay to start the WS Relay')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
start()
|
start()
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import * as auth from '@adxp/auth'
|
||||||
|
import {isWeb} from '../platform/detection'
|
||||||
|
import * as env from '../env'
|
||||||
|
|
||||||
|
const SCOPE = auth.writeCap(
|
||||||
|
'did:key:z6MkfRiFMLzCxxnw6VMrHK8pPFt4QAHS3jX3XM87y9rta6kP',
|
||||||
|
'did:example:microblog',
|
||||||
|
)
|
||||||
|
|
||||||
|
export async function isAuthed(authStore: auth.BrowserStore) {
|
||||||
|
return await authStore.hasUcan(SCOPE)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logout(authStore: auth.BrowserStore) {
|
||||||
|
await authStore.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseUrlForUcan() {
|
||||||
|
// @ts-ignore window is defined -prf
|
||||||
|
const fragment = window.location.hash
|
||||||
|
if (fragment.length < 1) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const ucan = await auth.parseLobbyResponseHashFragment(fragment)
|
||||||
|
// @ts-ignore window is defined -prf
|
||||||
|
window.location.hash = ''
|
||||||
|
return ucan
|
||||||
|
} catch (err) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestAppUcan(authStore: auth.BrowserStore) {
|
||||||
|
const did = await authStore.getDid()
|
||||||
|
if (isWeb) {
|
||||||
|
// @ts-ignore window is defined -prf
|
||||||
|
const redirectTo = window.location.origin
|
||||||
|
const fragment = auth.requestAppUcanHashFragment(did, SCOPE, redirectTo)
|
||||||
|
// @ts-ignore window is defined -prf
|
||||||
|
window.location.href = `${env.AUTH_LOBBY}#${fragment}`
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
// TODO
|
||||||
|
console.log('TODO')
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -1,5 +1,26 @@
|
||||||
import {MicroblogDelegator, MicroblogReader, auth} from '@adx/common'
|
// import {MicroblogDelegator, MicroblogReader, auth} from '@adx/common'
|
||||||
import * as ucan from 'ucans'
|
// 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 {
|
export class API {
|
||||||
userCfg?: UserConfig
|
userCfg?: UserConfig
|
||||||
|
@ -51,9 +72,9 @@ export interface SerializedUserConfig {
|
||||||
export class UserConfig {
|
export class UserConfig {
|
||||||
serverUrl?: string
|
serverUrl?: string
|
||||||
did?: string
|
did?: string
|
||||||
keypair?: ucan.EdKeypair
|
keypair?: any //ucan.EdKeypair
|
||||||
rootAuthToken?: string
|
rootAuthToken?: string
|
||||||
ucanStore?: ucan.Store
|
ucanStore?: any //ucan.Store
|
||||||
|
|
||||||
get hasWriteCaps() {
|
get hasWriteCaps() {
|
||||||
return Boolean(this.did && this.keypair && this.ucanStore)
|
return Boolean(this.did && this.keypair && this.ucanStore)
|
||||||
|
@ -62,10 +83,10 @@ export class UserConfig {
|
||||||
static async createTest(serverUrl: string) {
|
static async createTest(serverUrl: string) {
|
||||||
const cfg = new UserConfig()
|
const cfg = new UserConfig()
|
||||||
cfg.serverUrl = serverUrl
|
cfg.serverUrl = serverUrl
|
||||||
cfg.keypair = await ucan.EdKeypair.create()
|
cfg.keypair = true //await ucan.EdKeypair.create()
|
||||||
cfg.did = cfg.keypair.did()
|
cfg.did = cfg.keypair.did()
|
||||||
cfg.rootAuthToken = (await auth.claimFull(cfg.did, cfg.keypair)).encoded()
|
cfg.rootAuthToken = (await auth.claimFull(cfg.did, cfg.keypair)).encoded()
|
||||||
cfg.ucanStore = await ucan.Store.fromTokens([cfg.rootAuthToken])
|
cfg.ucanStore = true // await ucan.Store.fromTokens([cfg.rootAuthToken])
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,10 +109,10 @@ export class UserConfig {
|
||||||
async hydrate(state: SerializedUserConfig) {
|
async hydrate(state: SerializedUserConfig) {
|
||||||
this.serverUrl = state.serverUrl
|
this.serverUrl = state.serverUrl
|
||||||
if (state.secretKeyStr && state.rootAuthToken) {
|
if (state.secretKeyStr && state.rootAuthToken) {
|
||||||
this.keypair = ucan.EdKeypair.fromSecretKey(state.secretKeyStr)
|
this.keypair = true // ucan.EdKeypair.fromSecretKey(state.secretKeyStr)
|
||||||
this.did = this.keypair.did()
|
this.did = this.keypair.did()
|
||||||
this.rootAuthToken = state.rootAuthToken
|
this.rootAuthToken = state.rootAuthToken
|
||||||
this.ucanStore = await ucan.Store.fromTokens([this.rootAuthToken])
|
this.ucanStore = true // await ucan.Store.fromTokens([this.rootAuthToken])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
if (typeof process.env.REACT_APP_AUTH_LOBBY !== 'string') {
|
||||||
|
throw new Error('ENV: No auth lobby provided')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AUTH_LOBBY = process.env.REACT_APP_AUTH_LOBBY
|
|
@ -14,7 +14,7 @@ export function Home({navigation}: RootTabsScreenProps<'Home'>) {
|
||||||
title="Go to Jane's profile"
|
title="Go to Jane's profile"
|
||||||
onPress={() => navigation.navigate('Profile', {name: 'Jane'})}
|
onPress={() => navigation.navigate('Profile', {name: 'Jane'})}
|
||||||
/>
|
/>
|
||||||
<Button title="Logout" onPress={() => store.session.setAuthed(false)} />
|
<Button title="Logout" onPress={() => store.session.logout()} />
|
||||||
</View>
|
</View>
|
||||||
</Shell>
|
</Shell>
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,14 +12,9 @@ export const Login = observer(({navigation}: RootTabsScreenProps<'Login'>) => {
|
||||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Sign In</Text>
|
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Sign In</Text>
|
||||||
{store.session.uiError ?? <Text>{store.session.uiError}</Text>}
|
{store.session.uiError ?? <Text>{store.session.uiError}</Text>}
|
||||||
{store.session.uiState === 'idle' ? (
|
{!store.session.uiIsProcessing ? (
|
||||||
<>
|
<>
|
||||||
{store.session.hasAccount ?? (
|
<Button title="Login" onPress={() => store.session.login()} />
|
||||||
<Button
|
|
||||||
title="Login"
|
|
||||||
onPress={() => store.session.loadAccount()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
title="Sign Up"
|
title="Sign Up"
|
||||||
onPress={() => navigation.navigate('Signup')}
|
onPress={() => navigation.navigate('Signup')}
|
||||||
|
|
|
@ -13,13 +13,11 @@ export const Signup = observer(
|
||||||
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
<View style={{justifyContent: 'center', alignItems: 'center'}}>
|
||||||
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Create Account</Text>
|
<Text style={{fontSize: 20, fontWeight: 'bold'}}>Create Account</Text>
|
||||||
{store.session.uiError ?? <Text>{store.session.uiError}</Text>}
|
{store.session.uiError ?? <Text>{store.session.uiError}</Text>}
|
||||||
{store.session.uiState === 'idle' ? (
|
{!store.session.uiIsProcessing ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
title="Create new account"
|
title="Create new account"
|
||||||
onPress={() =>
|
onPress={() => store.session.login()}
|
||||||
store.session.createTestAccount('http://localhost:1986')
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
title="Log in to an existing account"
|
title="Log in to an existing account"
|
||||||
|
|
|
@ -4,14 +4,18 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {getEnv, IStateTreeNode} from 'mobx-state-tree'
|
import {getEnv, IStateTreeNode} from 'mobx-state-tree'
|
||||||
|
import * as auth from '@adxp/auth'
|
||||||
import {API} from '../api'
|
import {API} from '../api'
|
||||||
|
|
||||||
export class Environment {
|
export class Environment {
|
||||||
api = new API()
|
api = new API()
|
||||||
|
authStore?: auth.BrowserStore
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
async setup() {}
|
async setup() {
|
||||||
|
this.authStore = await auth.BrowserStore.load()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
} from './models/root-store'
|
} from './models/root-store'
|
||||||
import {Environment} from './env'
|
import {Environment} from './env'
|
||||||
import * as storage from './storage'
|
import * as storage from './storage'
|
||||||
|
import * as auth from '../api/auth'
|
||||||
|
|
||||||
const ROOT_STATE_STORAGE_KEY = 'root'
|
const ROOT_STATE_STORAGE_KEY = 'root'
|
||||||
|
|
||||||
|
@ -14,6 +15,7 @@ export async function setupState() {
|
||||||
let data: any
|
let data: any
|
||||||
|
|
||||||
const env = new Environment()
|
const env = new Environment()
|
||||||
|
await env.setup()
|
||||||
try {
|
try {
|
||||||
data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {}
|
data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {}
|
||||||
rootStore = RootStoreModel.create(data, env)
|
rootStore = RootStoreModel.create(data, env)
|
||||||
|
@ -27,6 +29,16 @@ export async function setupState() {
|
||||||
storage.save(ROOT_STATE_STORAGE_KEY, snapshot),
|
storage.save(ROOT_STATE_STORAGE_KEY, snapshot),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (env.authStore) {
|
||||||
|
const isAuthed = await auth.isAuthed(env.authStore)
|
||||||
|
rootStore.session.setAuthed(isAuthed)
|
||||||
|
const ucan = await auth.parseUrlForUcan()
|
||||||
|
if (ucan) {
|
||||||
|
await env.authStore.addUcan(ucan)
|
||||||
|
rootStore.session.setAuthed(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rootStore
|
return rootStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import {Instance, SnapshotOut, types, flow} from 'mobx-state-tree'
|
import {Instance, SnapshotOut, types, flow} from 'mobx-state-tree'
|
||||||
import {UserConfig} from '../../api'
|
// import {UserConfig} from '../../api'
|
||||||
|
import * as auth from '../../api/auth'
|
||||||
import {withEnvironment} from '../env'
|
import {withEnvironment} from '../env'
|
||||||
|
|
||||||
export const SessionModel = types
|
export const SessionModel = types
|
||||||
.model('Session')
|
.model('Session')
|
||||||
.props({
|
.props({
|
||||||
isAuthed: types.boolean,
|
isAuthed: types.boolean,
|
||||||
uiState: types.enumeration('idle', ['idle', 'working']),
|
uiIsProcessing: types.maybe(types.boolean),
|
||||||
uiError: types.maybe(types.string),
|
uiError: types.maybe(types.string),
|
||||||
|
|
||||||
// TODO: these should be stored somewhere secret
|
// TODO: these should be stored somewhere secret
|
||||||
|
@ -14,56 +15,84 @@ export const SessionModel = types
|
||||||
secretKeyStr: types.maybe(types.string),
|
secretKeyStr: types.maybe(types.string),
|
||||||
rootAuthToken: types.maybe(types.string),
|
rootAuthToken: types.maybe(types.string),
|
||||||
})
|
})
|
||||||
.views(self => ({
|
|
||||||
get hasAccount() {
|
|
||||||
return self.serverUrl && self.secretKeyStr && self.rootAuthToken
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
.extend(withEnvironment)
|
.extend(withEnvironment)
|
||||||
.actions(self => ({
|
.actions(self => ({
|
||||||
setAuthed: (v: boolean) => {
|
setAuthed: (v: boolean) => {
|
||||||
self.isAuthed = v
|
self.isAuthed = v
|
||||||
},
|
},
|
||||||
loadAccount: flow(function* () {
|
login: flow(function* () {
|
||||||
if (!self.hasAccount) {
|
self.uiIsProcessing = true
|
||||||
return false
|
|
||||||
}
|
|
||||||
self.uiState = 'working'
|
|
||||||
self.uiError = undefined
|
self.uiError = undefined
|
||||||
try {
|
try {
|
||||||
const cfg = yield UserConfig.hydrate({
|
if (!self.environment.authStore) {
|
||||||
serverUrl: self.serverUrl,
|
throw new Error('Auth store not initialized')
|
||||||
secretKeyStr: self.secretKeyStr,
|
}
|
||||||
rootAuthToken: self.rootAuthToken,
|
const res = yield auth.requestAppUcan(self.environment.authStore)
|
||||||
})
|
self.isAuthed = res
|
||||||
self.environment.api.setUserCfg(cfg)
|
self.uiIsProcessing = false
|
||||||
|
return res
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('Failed to request app ucan', e)
|
||||||
|
self.uiError = e.toString()
|
||||||
|
self.uiIsProcessing = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
logout: flow(function* () {
|
||||||
|
self.uiIsProcessing = true
|
||||||
|
self.uiError = undefined
|
||||||
|
try {
|
||||||
|
if (!self.environment.authStore) {
|
||||||
|
throw new Error('Auth store not initialized')
|
||||||
|
}
|
||||||
|
const res = yield auth.logout(self.environment.authStore)
|
||||||
|
self.isAuthed = false
|
||||||
|
self.uiIsProcessing = false
|
||||||
|
return res
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('Failed to log out', e)
|
||||||
|
self.uiError = e.toString()
|
||||||
|
self.uiIsProcessing = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
/*loadAccount: flow(function* () {
|
||||||
|
self.uiIsProcessing = true
|
||||||
|
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.isAuthed = true
|
||||||
self.uiState = 'idle'
|
self.uiIsProcessing = false
|
||||||
return true
|
return true
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('Failed to create test account', e)
|
console.error('Failed to create test account', e)
|
||||||
self.uiError = e.toString()
|
self.uiError = e.toString()
|
||||||
self.uiState = 'idle'
|
self.uiIsProcessing = false
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
createTestAccount: flow(function* (serverUrl: string) {
|
createTestAccount: flow(function* (_serverUrl: string) {
|
||||||
self.uiState = 'working'
|
self.uiIsProcessing = true
|
||||||
self.uiError = undefined
|
self.uiError = undefined
|
||||||
try {
|
try {
|
||||||
const cfg = yield UserConfig.createTest(serverUrl)
|
// const cfg = yield UserConfig.createTest(serverUrl)
|
||||||
const state = yield cfg.serialize()
|
// const state = yield cfg.serialize()
|
||||||
self.serverUrl = state.serverUrl
|
// self.serverUrl = state.serverUrl
|
||||||
self.secretKeyStr = state.secretKeyStr
|
// self.secretKeyStr = state.secretKeyStr
|
||||||
self.rootAuthToken = state.rootAuthToken
|
// self.rootAuthToken = state.rootAuthToken
|
||||||
self.isAuthed = true
|
self.isAuthed = true
|
||||||
self.environment.api.setUserCfg(cfg)
|
// self.environment.api.setUserCfg(cfg)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('Failed to create test account', e)
|
console.error('Failed to create test account', e)
|
||||||
self.uiError = e.toString()
|
self.uiError = e.toString()
|
||||||
}
|
}
|
||||||
self.uiState = 'idle'
|
self.uiIsProcessing = false
|
||||||
}),
|
}),*/
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export interface Session extends Instance<typeof SessionModel> {}
|
export interface Session extends Instance<typeof SessionModel> {}
|
||||||
|
|
Loading…
Reference in New Issue