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"