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:
Paul Frazee
2023-01-30 18:30:42 -06:00
committed by GitHub
parent d563beb239
commit 43bc272615
11 changed files with 103 additions and 64 deletions
+19 -3
View File
@@ -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
View File
@@ -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(() => {
+3 -2
View File
@@ -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 {
+15
View File
@@ -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
*/
+1
View File
@@ -94,6 +94,7 @@ export class MeModel {
this.avatar = ''
}
})
this.mainFeed.clear()
this.mainFeed = new FeedModel(this.rootStore, 'home', {
algorithm: 'reverse-chronological',
})
+29 -3
View File
@@ -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
View File
@@ -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()
})
}
}
+3 -3
View File
@@ -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) {
+1 -1
View File
@@ -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})
+2
View File
@@ -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 = () => {
+6 -1
View File
@@ -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"