diff --git a/__tests__/accounts.test.tsx b/__tests__/accounts.test.tsx
index f3ecb6af43..8e656a0304 100644
--- a/__tests__/accounts.test.tsx
+++ b/__tests__/accounts.test.tsx
@@ -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(, rootStore)
+ const {getAllByTestId, getByTestId} = render(, 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(, rootStore)
+ const {getAllByTestId, getByTestId} = render(, 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,
diff --git a/src/state/index.ts b/src/state/index.ts
index 78fba2ecf6..f8243382fe 100644
--- a/src/state/index.ts
+++ b/src/state/index.ts
@@ -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(() => {
diff --git a/src/state/lib/api.ts b/src/state/lib/api.ts
index e498bef1bb..a88fae9282 100644
--- a/src/state/lib/api.ts
+++ b/src/state/lib/api.ts
@@ -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 {
diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts
index 6210598223..bab308e989 100644
--- a/src/state/models/feed-view.ts
+++ b/src/state/models/feed-view.ts
@@ -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
*/
diff --git a/src/state/models/me.ts b/src/state/models/me.ts
index 0d0c1d1de7..3a431c6773 100644
--- a/src/state/models/me.ts
+++ b/src/state/models/me.ts
@@ -94,6 +94,7 @@ export class MeModel {
this.avatar = ''
}
})
+ this.mainFeed.clear()
this.mainFeed = new FeedModel(this.rootStore, 'home', {
algorithm: 'reverse-chronological',
})
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index c4798ad0b4..7866c4704d 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -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(throwawayInst)
export const RootStoreProvider = RootStoreContext.Provider
export const useStores = () => useContext(RootStoreContext)
diff --git a/src/state/models/session.ts b/src/state/models/session.ts
index bc0a9123f1..075e8c711e 100644
--- a/src/state/models/session.ts
+++ b/src/state/models/session.ts
@@ -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 {
- 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()
+ })
}
}
diff --git a/src/view/com/login/Signin.tsx b/src/view/com/login/Signin.tsx
index 2dfb012e84..64a41a1a34 100644
--- a/src/view/com/login/Signin.tsx
+++ b/src/view/com/login/Signin.tsx
@@ -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) {
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 384ee15e15..ff6bd881e3 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -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})
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index d659d25d46..8190b48a70 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -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 = () => {
diff --git a/yarn.lock b/yarn.lock
index 5eed81ec7e..1733714fb2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"