Fixes to session management (#117)
* Update session-management to solve incorrectly dropped sessions * Reset the nav on account switch * Reset the feed on me.load() * Update tests to reflect new account-switching behavior
This commit is contained in:
@@ -180,7 +180,7 @@ describe('Account flows', () => {
|
||||
|
||||
// signed in
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('settingsScreen')).toBeTruthy() // we go back to settings in this situation
|
||||
expect(getByTestId('homeFeed')).toBeTruthy() // we go back to settings in this situation
|
||||
expect(rootStore?.me?.displayName).toBe('Bob')
|
||||
expect(rootStore?.me?.handle).toBe('bob.test')
|
||||
expect(rootStore?.session.accounts.length).toBe(2)
|
||||
@@ -191,7 +191,15 @@ describe('Account flows', () => {
|
||||
})
|
||||
|
||||
it('can instantly switch between accounts', async () => {
|
||||
const {getByTestId} = render(<MobileShell />, rootStore)
|
||||
const {getAllByTestId, getByTestId} = render(<MobileShell />, rootStore)
|
||||
await waitFor(() => expect(getByTestId('homeFeed')).toBeTruthy(), WAIT_OPTS)
|
||||
|
||||
// open side menu
|
||||
fireEvent.press(getAllByTestId('viewHeaderBackOrMenuBtn')[0])
|
||||
await waitFor(() => expect(getByTestId('menuView')).toBeTruthy(), WAIT_OPTS)
|
||||
|
||||
// nav to settings
|
||||
fireEvent.press(getByTestId('menuItemButton-Settings'))
|
||||
await waitFor(
|
||||
() => expect(getByTestId('settingsScreen')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
@@ -212,7 +220,15 @@ describe('Account flows', () => {
|
||||
})
|
||||
|
||||
it('will prompt for a password if you sign out', async () => {
|
||||
const {getByTestId} = render(<MobileShell />, rootStore)
|
||||
const {getAllByTestId, getByTestId} = render(<MobileShell />, rootStore)
|
||||
await waitFor(() => expect(getByTestId('homeFeed')).toBeTruthy(), WAIT_OPTS)
|
||||
|
||||
// open side menu
|
||||
fireEvent.press(getAllByTestId('viewHeaderBackOrMenuBtn')[0])
|
||||
await waitFor(() => expect(getByTestId('menuView')).toBeTruthy(), WAIT_OPTS)
|
||||
|
||||
// nav to settings
|
||||
fireEvent.press(getByTestId('menuItemButton-Settings'))
|
||||
await waitFor(
|
||||
() => expect(getByTestId('settingsScreen')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
|
||||
+2
-11
@@ -1,6 +1,6 @@
|
||||
import {autorun} from 'mobx'
|
||||
import {Platform} from 'react-native'
|
||||
import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
|
||||
import {sessionClient as SessionAtpApi} from '@atproto/api'
|
||||
import {RootStoreModel} from './models/root-store'
|
||||
import * as libapi from './lib/api'
|
||||
import * as storage from './lib/storage'
|
||||
@@ -19,7 +19,7 @@ export async function setupState(serviceUri = DEFAULT_SERVICE) {
|
||||
|
||||
libapi.doPolyfill()
|
||||
|
||||
const api = AtpApi.service(serviceUri) as SessionServiceClient
|
||||
const api = SessionAtpApi.service(serviceUri)
|
||||
rootStore = new RootStoreModel(api)
|
||||
try {
|
||||
data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {}
|
||||
@@ -38,15 +38,6 @@ export async function setupState(serviceUri = DEFAULT_SERVICE) {
|
||||
.catch((e: any) => {
|
||||
rootStore.log.warn('Failed initial connect', e)
|
||||
})
|
||||
// @ts-ignore .on() is correct -prf
|
||||
api.sessionManager.on('session', () => {
|
||||
if (!api.sessionManager.session && rootStore.session.hasSession) {
|
||||
// reset session
|
||||
rootStore.session.clear()
|
||||
} else if (api.sessionManager.session) {
|
||||
rootStore.session.updateAuthTokens(api.sessionManager.session)
|
||||
}
|
||||
})
|
||||
|
||||
// track changes & save to storage
|
||||
autorun(() => {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
// import {ReactNativeStore} from './auth'
|
||||
import {
|
||||
sessionClient as AtpApi,
|
||||
import AtpApi, {
|
||||
sessionClient as SessionAtpApi,
|
||||
AppBskyEmbedImages,
|
||||
AppBskyEmbedExternal,
|
||||
} from '@atproto/api'
|
||||
@@ -21,6 +21,7 @@ const TIMEOUT = 10e3 // 10s
|
||||
|
||||
export function doPolyfill() {
|
||||
AtpApi.xrpc.fetch = fetchHandler
|
||||
SessionAtpApi.xrpc.fetch = fetchHandler
|
||||
}
|
||||
|
||||
export interface ExternalEmbedDraft {
|
||||
|
||||
@@ -245,6 +245,21 @@ export class FeedModel {
|
||||
// public api
|
||||
// =
|
||||
|
||||
/**
|
||||
* Nuke all data
|
||||
*/
|
||||
clear() {
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasNewLatest = false
|
||||
this.hasLoaded = false
|
||||
this.error = ''
|
||||
this.hasMore = true
|
||||
this.loadMoreCursor = undefined
|
||||
this.pollCursor = undefined
|
||||
this.feed = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Load for first render
|
||||
*/
|
||||
|
||||
@@ -94,6 +94,7 @@ export class MeModel {
|
||||
this.avatar = ''
|
||||
}
|
||||
})
|
||||
this.mainFeed.clear()
|
||||
this.mainFeed = new FeedModel(this.rootStore, 'home', {
|
||||
algorithm: 'reverse-chronological',
|
||||
})
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
*/
|
||||
|
||||
import {makeAutoObservable} from 'mobx'
|
||||
import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
|
||||
import {
|
||||
sessionClient as SessionAtpApi,
|
||||
SessionServiceClient,
|
||||
} from '@atproto/api'
|
||||
import {createContext, useContext} from 'react'
|
||||
import {DeviceEventEmitter, EmitterSubscription} from 'react-native'
|
||||
import BackgroundFetch from 'react-native-background-fetch'
|
||||
@@ -19,6 +22,7 @@ import {OnboardModel} from './onboard'
|
||||
import {isNetworkError} from '../../lib/errors'
|
||||
|
||||
export class RootStoreModel {
|
||||
api: SessionServiceClient
|
||||
log = new LogModel()
|
||||
session = new SessionModel(this)
|
||||
nav = new NavigationModel()
|
||||
@@ -28,7 +32,9 @@ export class RootStoreModel {
|
||||
profiles = new ProfilesViewModel(this)
|
||||
linkMetas = new LinkMetasViewModel(this)
|
||||
|
||||
constructor(public api: SessionServiceClient) {
|
||||
constructor(api: SessionServiceClient) {
|
||||
this.api = api // to keep typescript from whining
|
||||
this.setAPI(api)
|
||||
makeAutoObservable(this, {
|
||||
api: false,
|
||||
resolveName: false,
|
||||
@@ -38,6 +44,24 @@ export class RootStoreModel {
|
||||
this.initBgFetch()
|
||||
}
|
||||
|
||||
setAPI(api: SessionServiceClient) {
|
||||
if (this.api) {
|
||||
this.api.sessionManager.removeAllListeners('session')
|
||||
}
|
||||
this.api = api
|
||||
this.api.sessionManager.on('session', this.onSessionChange.bind(this))
|
||||
}
|
||||
|
||||
onSessionChange() {
|
||||
if (!this.api.sessionManager.session && this.session.hasSession) {
|
||||
this.log.debug('Session invalidated, logging the user out')
|
||||
this.session.clear()
|
||||
} else if (this.api.sessionManager.session) {
|
||||
this.log.debug('Session refreshed, updating auth tokens')
|
||||
this.session.updateAuthTokens(this.api.sessionManager.session)
|
||||
}
|
||||
}
|
||||
|
||||
async resolveName(didOrHandle: string) {
|
||||
if (!didOrHandle) {
|
||||
throw new Error('Invalid handle: ""')
|
||||
@@ -147,7 +171,9 @@ export class RootStoreModel {
|
||||
}
|
||||
}
|
||||
|
||||
const throwawayInst = new RootStoreModel(AtpApi.service('http://localhost')) // this will be replaced by the loader, we just need to supply a value at init
|
||||
const throwawayInst = new RootStoreModel(
|
||||
SessionAtpApi.service('http://localhost'),
|
||||
) // this will be replaced by the loader, we just need to supply a value at init
|
||||
const RootStoreContext = createContext<RootStoreModel>(throwawayInst)
|
||||
export const RootStoreProvider = RootStoreContext.Provider
|
||||
export const useStores = () => useContext(RootStoreContext)
|
||||
|
||||
+22
-40
@@ -1,8 +1,7 @@
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {
|
||||
sessionClient as AtpApi,
|
||||
import AtpApi, {
|
||||
sessionClient as SessionAtpApi,
|
||||
Session,
|
||||
SessionServiceClient,
|
||||
ComAtprotoServerGetAccountsConfig as GetAccountsConfig,
|
||||
} from '@atproto/api'
|
||||
import {isObj, hasProp} from '../lib/type-guards'
|
||||
@@ -95,6 +94,7 @@ export class SessionModel {
|
||||
|
||||
setState(data: SessionData) {
|
||||
this.data = data
|
||||
this.addSessionToAccounts()
|
||||
}
|
||||
|
||||
setOnline(online: boolean, attemptingConnect?: boolean) {
|
||||
@@ -124,7 +124,12 @@ export class SessionModel {
|
||||
|
||||
try {
|
||||
const serviceUri = new URL(this.data.service)
|
||||
this.rootStore.api.xrpc.uri = serviceUri
|
||||
const api = SessionAtpApi.service(serviceUri)
|
||||
api.sessionManager.set({
|
||||
refreshJwt: this.data.refreshJwt,
|
||||
accessJwt: this.data.accessJwt,
|
||||
})
|
||||
this.rootStore.setAPI(api)
|
||||
} catch (e: any) {
|
||||
this.rootStore.log.error(
|
||||
`Invalid service URL: ${this.data.service}. Resetting session.`,
|
||||
@@ -133,11 +138,6 @@ export class SessionModel {
|
||||
this.clear()
|
||||
return false
|
||||
}
|
||||
|
||||
this.rootStore.api.sessionManager.set({
|
||||
refreshJwt: this.data.refreshJwt,
|
||||
accessJwt: this.data.accessJwt,
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -212,17 +212,9 @@ export class SessionModel {
|
||||
if (this.rootStore.me.did !== sess.data.did) {
|
||||
this.rootStore.me.clear()
|
||||
}
|
||||
this.rootStore.me
|
||||
.load()
|
||||
.catch(e => {
|
||||
this.rootStore.log.error(
|
||||
'Failed to fetch local user information',
|
||||
e,
|
||||
)
|
||||
})
|
||||
.then(() => {
|
||||
this.addSessionToAccounts()
|
||||
})
|
||||
this.rootStore.me.load().then(() => {
|
||||
this.addSessionToAccounts()
|
||||
})
|
||||
return true // success
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -242,7 +234,7 @@ export class SessionModel {
|
||||
* Helper to fetch the accounts config settings from an account.
|
||||
*/
|
||||
async describeService(service: string): Promise<ServiceDescription> {
|
||||
const api = AtpApi.service(service) as SessionServiceClient
|
||||
const api = AtpApi.service(service)
|
||||
const res = await api.com.atproto.server.getAccountsConfig({})
|
||||
return res.data
|
||||
}
|
||||
@@ -259,7 +251,7 @@ export class SessionModel {
|
||||
handle: string
|
||||
password: string
|
||||
}) {
|
||||
const api = AtpApi.service(service) as SessionServiceClient
|
||||
const api = AtpApi.service(service)
|
||||
const res = await api.com.atproto.session.create({handle, password})
|
||||
if (res.data.accessJwt && res.data.refreshJwt) {
|
||||
this.setState({
|
||||
@@ -271,14 +263,9 @@ export class SessionModel {
|
||||
})
|
||||
this.configureApi()
|
||||
this.setOnline(true, false)
|
||||
this.rootStore.me
|
||||
.load()
|
||||
.catch(e => {
|
||||
this.rootStore.log.error('Failed to fetch local user information', e)
|
||||
})
|
||||
.then(() => {
|
||||
this.addSessionToAccounts()
|
||||
})
|
||||
this.rootStore.me.load().then(() => {
|
||||
this.addSessionToAccounts()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +278,7 @@ export class SessionModel {
|
||||
}
|
||||
|
||||
// test that the session is good
|
||||
const api = AtpApi.service(account.service)
|
||||
const api = SessionAtpApi.service(account.service)
|
||||
api.sessionManager.set({
|
||||
refreshJwt: account.refreshJwt,
|
||||
accessJwt: account.accessJwt,
|
||||
@@ -339,7 +326,7 @@ export class SessionModel {
|
||||
handle: string
|
||||
inviteCode?: string
|
||||
}) {
|
||||
const api = AtpApi.service(service) as SessionServiceClient
|
||||
const api = AtpApi.service(service)
|
||||
const res = await api.com.atproto.account.create({
|
||||
handle,
|
||||
password,
|
||||
@@ -356,14 +343,9 @@ export class SessionModel {
|
||||
})
|
||||
this.rootStore.onboard.start()
|
||||
this.configureApi()
|
||||
this.rootStore.me
|
||||
.load()
|
||||
.catch(e => {
|
||||
this.rootStore.log.error('Failed to fetch local user information', e)
|
||||
})
|
||||
.then(() => {
|
||||
this.addSessionToAccounts()
|
||||
})
|
||||
this.rootStore.me.load().then(() => {
|
||||
this.addSessionToAccounts()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import * as EmailValidator from 'email-validator'
|
||||
import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
|
||||
import AtpApi from '@atproto/api'
|
||||
import {useAnalytics} from '@segment/analytics-react-native'
|
||||
import {LogoTextHero} from './Logo'
|
||||
import {Text} from '../util/text/Text'
|
||||
@@ -471,7 +471,7 @@ const ForgotPasswordForm = ({
|
||||
setIsProcessing(true)
|
||||
|
||||
try {
|
||||
const api = AtpApi.service(serviceUrl) as SessionServiceClient
|
||||
const api = AtpApi.service(serviceUrl)
|
||||
await api.com.atproto.account.requestPasswordReset({email})
|
||||
onEmailSent()
|
||||
} catch (e: any) {
|
||||
@@ -602,7 +602,7 @@ const SetNewPasswordForm = ({
|
||||
setIsProcessing(true)
|
||||
|
||||
try {
|
||||
const api = AtpApi.service(serviceUrl) as SessionServiceClient
|
||||
const api = AtpApi.service(serviceUrl)
|
||||
await api.com.atproto.account.resetPassword({token: resetCode, password})
|
||||
onPasswordSet()
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -69,7 +69,7 @@ export const Home = observer(function Home({
|
||||
store.me.mainFeed.setup()
|
||||
}
|
||||
return cleanup
|
||||
}, [visible, store, navIdx, doPoll, wasVisible])
|
||||
}, [visible, store, store.me.mainFeed, navIdx, doPoll, wasVisible])
|
||||
|
||||
const onPressCompose = (imagesOpen?: boolean) => {
|
||||
store.shell.openComposer({imagesOpen})
|
||||
|
||||
@@ -39,11 +39,13 @@ export const Settings = observer(function Settings({
|
||||
setIsSwitching(true)
|
||||
if (await store.session.resumeSession(acct)) {
|
||||
setIsSwitching(false)
|
||||
store.nav.tab.fixedTabReset()
|
||||
Toast.show(`Signed in as ${acct.displayName || acct.handle}`)
|
||||
return
|
||||
}
|
||||
setIsSwitching(false)
|
||||
Toast.show('Sorry! We need you to enter your password.')
|
||||
store.nav.tab.fixedTabReset()
|
||||
store.session.clear()
|
||||
}
|
||||
const onPressAddAccount = () => {
|
||||
|
||||
@@ -12813,11 +12813,16 @@ tslib@^1.8.1:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0:
|
||||
tslib@^2.0.1, tslib@^2.0.3, tslib@^2.4.0:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
|
||||
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
|
||||
|
||||
tslib@^2.1.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
|
||||
integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
|
||||
|
||||
tsutils@^3.21.0:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
|
||||
|
||||
Reference in New Issue
Block a user