about summary refs log tree commit diff
path: root/src/state/models/session.ts
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-02-22 14:23:57 -0600
committerGitHub <noreply@github.com>2023-02-22 14:23:57 -0600
commitf28334739b107f3e9f7b6ca2670778dba280600d (patch)
tree4e1563242e1a041c5d5483ab018123170dcb3fc8 /src/state/models/session.ts
parent7916b26aadb7e003728d9dc653ab8b8deabf4076 (diff)
downloadvoidsky-f28334739b107f3e9f7b6ca2670778dba280600d.tar.zst
Merge main into the Web PR (#230)
* Update to RN 71.1.0 (#100)

* Update to RN 71

* Adds missing lint plugin

* Add missing native changes

* Bump @atproto/api@0.0.7 (#112)

* Image not loading on swipe (#114)

* Adds prefetching to images

* Adds image prefetch

* bugfix for images not showing on swipe

* Fixes prefetch bug

* Update src/view/com/util/PostEmbeds.tsx

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

* 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

* Increase max image resolutions and sizes (#118)

* Slightly increase the hitslop for post controls

* Fix character counter color in dark mode

* Update login to use new session.create api, which enables email login (close #93) (#119)

* Replaces the alert with dropdown for profile image and banner (#123)

* replaces the alert with dropdown for profile image and banner

* lint

* Fix to ordering of images in the embed grid (#121)

* Add explicit link-embed controls to the composer (#120)

* Add explicit link-embed controls

* Update the target rez/size of link embed thumbs

* Remove the alert before publishing without a link card

* [Draft] Fixes image failing on reupload issue (#128)

* Fixes image failing on reupload issue

* Use tmp folder instead of documents

* lint

* Image performance improvements (#126)

* Switch out most images for FastImage

* Add image loading placeholders

* Fix tests

* Collection of fixes to list rendering (#127)

* Fix bug that caused endless spinners in profile feeds

* Bundle fetches of suggested actors into one update

* Fixes to suggested follow rendering

* Fix missing replacement of flex:1 to height:100

* Fixes to navigation swipes (#129)

* Nav swipe: increase the distance traveled in response to gesture movement.

This causes swipes to feel faster and more responsive.

* Fix: fully clamp the swipe against the edge

* Improve the performance of swipes by skipping the interaction manager

* Adds dark mode to the edit screen (#130)

* Adds dark mode to edit screen

* lint

* lint

* lint

* Reduce render cost of post controls and improve perceived responsiveness (#132)

* Move post control animations into conditional render and increase perceived responsiveness

* Remove log

* Adds dark mode to the dropdown (#131)

* Adds dark mode to the bottom sheet

* Make background button lighter (like before)

* lint

* Fix bug in lightbox rendering (#133)

* Fix layout in onboarding to not overflow the footer

* Configure feed FlatList (removeClippedSubviews=true) to improve scroll performance (#136)

* Disable like/repost animations to see if theyre causing #135 (#137)

* Composer: mention tagging now works in middle of text (close #105) (#139)

* Implement account deletion (#141)

* Fix photo & camera permission management (#140)

* Check photo & camera perms and alert the user if not available (close #64)

- Adds perms checks with a prompt to update settings if needed
- Moves initial access of photos in the composer so that the initial prompt
  occurs at an intuitive time.

* Add react-native-permissions test mock

* Fix issue causing multiple access requests

* Use longer var names

* Update podfile.lock

* Lint fix

* Move photo perm request in composer to the gallery btn instead of when the carousel is opened

* Adds more tracking all around the app (#142)

* Adds more tracking all around the app

* more events

* lint

* using better analytics naming

* missed file

* more fixes

* Calculate image aspect ratio on load (#146)

* Calculate image aspect ratio on load

* Move aspect ratio bounds to constants

* Adds detox testing and instructions (#147)

* Adds detox testing and instructions

* lint

* lint

* Error cleanup (close #79) (#148)

* Avoid surfacing errors to the user when it's not critical

* Remove now-unused GetAssertionsView

* Apply cleanError() consistently

* Give a better error message for Upstream Failures (http status 502)

* Hide errors in notifications because they're not useful

* More e2e tests (create account) (#150)

* Adds respots under the 'post' tab under profile (#158)

* Adds dark mode to delete account screen (#159)

* 87 dark mode edit profile (#162)

* Adds dark mode to delete account screen

* Adds one more missed darkmode

* more fixes

* Remove fallback gradient on external links without thumbs (#164)

* Remove fallback gradient on external links without thumbs

* Remove fallback gradient on external links without thumbs in the composer preview

* Fix refresh behavior around a series of models (repost, graph, vote) (#163)

* Fix refresh behavior around a series of models (repost, graph, vote)

* Fix cursor behavior in reposted-by view

* Fixes issue where retrying on image upload fails (#166)

* Fixes issue where retrying on image upload fails

* Lint, longer test time

* Longer waitfor time in tests

* even longer timeout

* longer timeout

* missed file

* Update src/view/com/composer/ComposePost.tsx

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

* Update src/view/com/composer/ComposePost.tsx

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

* 154 cached image profile (#167)

* Fixes issue where retrying on image upload fails

* Lint, longer test time

* Longer waitfor time in tests

* even longer timeout

* longer timeout

* missed file

* Fixes image cache error on second try for profile screen

* lint

* lint

* lint

* Refactor session management to use a new "Agent" API (#165)

* Add the atp-agent implementation (temporarily in this repo)

* Rewrite all session & API management to use the new atp-agent

* Update tests for the atp-agent refactor

* Refactor management of session-related state. Includes:
- More careful management of when state is cleared or fetched
- Debug logging to help trace future issues
- Clearer APIs overall

* Bubble session-expiration events to the user and display a toast to explain

* Switch to the new @atproto/api@0.1.0

* Minor aesthetic cleanup in SessionModel

* Wire up ReportAccount and ReportPost (#168)

* Fixes embeds for youtube channels (#169)

* Bump app ios version to 1.1 (needed after app store submission)

* Fix potential issues with promise guards when an error occurs (#170)

* Refactor models to use bundleAsync and lock regions (#171)

* Fix to an edge case with feed re-ordering for threads (#172)

* 151 fix youtube channel embed (#173)

* Fixes embeds for youtube channels

* Tests for youtube extract meta

* lint

* Add 'doesnt use non-exempt encryption' to ios config

* Rework the search UI and add  (#174)

* Add search tab and move icon to footer

* Remove subtitles from view header

* Remove unused code

* Clean up UI of search screen

* Search: give better user feedback to UI state and add a cancel button

* Add WhoToFollow section to search

* Add a temporary SuggestedPosts solution using the patented 'bsky team algo'

* Trigger reload of suggested content in search on open

* Wait five min between reloading discovery content

* Reduce weight of solid search icon in footer

* Fix lint

* Fix tests

* 151 feat youtube embed iframe (#176)

* youtube embed iframe temp commit

* Fixes styling and code cleanup

* lint

* Now clicking between the pause and settings button doesn't trigger the parent

* use modest branding (less yt logos)

* Stop playing the video once there's a navigation event

* Make sure the iframe is unmounted on any navigation event

* fixes tests

* lint

* Add scroll-to-top for all screens (#177)

* Adds hardcoded suggested list (#178)

* Adds hardcoded suggested list

* Update suggested-actors-view to support page sizes smaller than the hardcoded list

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

* more robust centering of the play button (#181)

Co-authored-by: Aryan Goharzad <arrygoo@gmail.com>

* Bundle of UI modifications (#175)

* Adjust visual balance of SuggestedPosts and WhoToFollow

* Fix bug in the discovery load trigger

* Adjust search header aesthetic and have it scroll away

* More visual balance tweaks on the search page

* Even more visual balance tweaks on the search page

* Hide the footer on scroll in search

* Ditch the composer prompt buttons in the home feed

* Center the view header title

* Hide header on scroll on the home feed

* Fix e2e tests

* Fix home feed positioning (closes #189) (#195)

* Fix home feed positioning for floating header

* Fix positioning of errors in home feed

* Fix lint

* Don't show new-content notification for reposts (close #179) (#197)

* Show the splash screen during session resumption (close #186) (#199)

* Fix to suggested follows: chunk the hardcoded fetches to 25 at a time (close #196) (#198)

* UI updates to the floating action button (#201)

* Update FAB to use a plus icon and not drop shadow

* Update FAB positioning to be more consistent in different shell modes

* Animate the FAB's repositioning

* Remove the 'loading' placeholder from images as it degraded feed perf (#202)

* Remove the 'loading' placeholder from images as it degraded feed perf

* Remove references

* Fix RN bug that causes home feed not to load more; also fix home feed load view. (#208)

RN has a bug where rendering a flatlist with an empty array appears to break its
virtual list windowing behaviors. See https://stackoverflow.com/a/67873596

* Only give the loading spinner on the home feed during PTR (#207)

(cherry picked from commit b7a5da12fdfacef74873b5cf6d75f20d259bde0e)

* Implement our own lifecycle tracking to ensure it never fires while the app is backgrounded (close #193) (#211)

* Push notification fixes (#210)

* Fix to when screen analytics events are firing

* Fix: dont trigger update state when backgrounded

* Small fix to notifee API usage

* Fix: properly load notification info for push card

* Add feedback link to main menu (close #191) (#212)

* Add "follows you" information and sync follow state between views (#215)

* Bump @atproto/api@0.1.2 and update API usage

* Add 'follows you' pill to profile header (close #110)

* Add 'follows you' to followers and follows (close #103)

* Update reposted-by and liked-by views to use the same components as followers and following

* Create a local follows cache MyFollowsModel to keep views in sync (close #205)

* Add incremental hydration to the MyFollows model

* Fix tests

* Update deps

* Fix lint

* Fix to paginated fetches

* Fix reference

* Fix potential state-desync issue

* Fixes to notifications (#216)

* Improve push-notification for follows

* Refresh notifications on screen open (close #214)

* Avoid showing loader more than needed in post threads

* Refactor notification polling to handle view-state more effectively

* Delete a bunch of tests taht werent adding value

* Remove the accounts integration test; we'll use the e2e test instead

* Load latest in notifications when the screen is open rather than full refresh

* Randomize hard-coded suggested follows (#226)

* Ensure follows are loaded before filtering hardcoded suggestions

* Randomize hard-coded suggested profiles (close #219)

* Sanitizes posts on publish and render (#217)

* Sanatizes posts on publish and render

* lint

* lint and added sanitize to thread view as well

* adjusts indices based on replaced text

* Woops, fixes a bug

* bugfix + cleanup

* comment

* lint

* move sanitize text to later in the flow

* undo changes to compose post

* Add RichText library building upon the sanitizePost library method

* Add lodash.clonedeep dep

* Switch to RichText processing on record load & render

* Fix lint

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

* A group of notifications fixes (#227)

* Fix: don't group together notifications that can't visually be grouped (close #221)

* Mark all notifications read on PTR

* Small optimization: useCallback and useMemo in posts feed

* Add loading spinner to footer of notifications (close #222)

* Fix to scrolling to posts within a thread (#228)

* Fix: render the entire thread at start so that scrollToIndex works always (close #270)

* Visual fixes to thread 'load more'

* A few small perf improvements to thread rendering

* Fix lint

* 1.2

* Remove unused logger lib

* Remove state-mock

* Type fixes

* Reorganize the folder structure for lib and switch to typescript path aliases

* Move build-flags into lib

* Move to the state path alias

* Add view path alias

* Fix lint

* iOS build fixes

* Wrap analytics in native/web splitter and re-enable in all view code

* Add web version of react-native-webview

* Add web split for version number

* Fix BlurView import for web

* Add web split for fastimage

* Create web split for permissions lib

* Fix for web high priority images

---------

Co-authored-by: Aryan Goharzad <arrygoo@gmail.com>
Diffstat (limited to 'src/state/models/session.ts')
-rw-r--r--src/state/models/session.ts414
1 files changed, 200 insertions, 214 deletions
diff --git a/src/state/models/session.ts b/src/state/models/session.ts
index bc0a9123f..6e816120d 100644
--- a/src/state/models/session.ts
+++ b/src/state/models/session.ts
@@ -1,25 +1,22 @@
-import {makeAutoObservable, runInAction} from 'mobx'
+import {makeAutoObservable} from 'mobx'
 import {
-  sessionClient as AtpApi,
-  Session,
-  SessionServiceClient,
+  AtpAgent,
+  AtpSessionEvent,
+  AtpSessionData,
   ComAtprotoServerGetAccountsConfig as GetAccountsConfig,
 } from '@atproto/api'
-import {isObj, hasProp} from '../lib/type-guards'
+import normalizeUrl from 'normalize-url'
+import {isObj, hasProp} from 'lib/type-guards'
 import {z} from 'zod'
 import {RootStoreModel} from './root-store'
-import {isNetworkError} from '../../lib/errors'
 
 export type ServiceDescription = GetAccountsConfig.OutputSchema
 
-export const sessionData = z.object({
+export const activeSession = z.object({
   service: z.string(),
-  refreshJwt: z.string(),
-  accessJwt: z.string(),
-  handle: z.string(),
   did: z.string(),
 })
-export type SessionData = z.infer<typeof sessionData>
+export type ActiveSession = z.infer<typeof activeSession>
 
 export const accountData = z.object({
   service: z.string(),
@@ -32,18 +29,24 @@ export const accountData = z.object({
 })
 export type AccountData = z.infer<typeof accountData>
 
+interface AdditionalAccountData {
+  displayName?: string
+  aviUrl?: string
+}
+
 export class SessionModel {
   /**
-   * Current session data
+   * Currently-active session
    */
-  data: SessionData | null = null
+  data: ActiveSession | null = null
   /**
-   * A listing of the currently & previous sessions, used for account switching
+   * A listing of the currently & previous sessions
    */
   accounts: AccountData[] = []
-  online = false
-  attemptingConnect = false
-  private _connectPromise: Promise<boolean> | undefined
+  /**
+   * Flag to indicate if we're doing our initial-load session resumption
+   */
+  isResumingSession = false
 
   constructor(public rootStore: RootStoreModel) {
     makeAutoObservable(this, {
@@ -53,8 +56,22 @@ export class SessionModel {
     })
   }
 
+  get currentSession() {
+    if (!this.data) {
+      return undefined
+    }
+    const {did, service} = this.data
+    return this.accounts.find(
+      account =>
+        normalizeUrl(account.service) === normalizeUrl(service) &&
+        account.did === did &&
+        !!account.accessJwt &&
+        !!account.refreshJwt,
+    )
+  }
+
   get hasSession() {
-    return this.data !== null
+    return !!this.currentSession && !!this.rootStore.agent.session
   }
 
   get hasAccounts() {
@@ -75,8 +92,8 @@ export class SessionModel {
   hydrate(v: unknown) {
     this.accounts = []
     if (isObj(v)) {
-      if (hasProp(v, 'data') && sessionData.safeParse(v.data)) {
-        this.data = v.data as SessionData
+      if (hasProp(v, 'data') && activeSession.safeParse(v.data)) {
+        this.data = v.data as ActiveSession
       }
       if (hasProp(v, 'accounts') && Array.isArray(v.accounts)) {
         for (const account of v.accounts) {
@@ -90,92 +107,96 @@ export class SessionModel {
 
   clear() {
     this.data = null
-    this.setOnline(false)
   }
 
-  setState(data: SessionData) {
-    this.data = data
-  }
-
-  setOnline(online: boolean, attemptingConnect?: boolean) {
-    this.online = online
-    if (typeof attemptingConnect === 'boolean') {
-      this.attemptingConnect = attemptingConnect
-    }
-  }
-
-  updateAuthTokens(session: Session) {
-    if (this.data) {
-      this.setState({
-        ...this.data,
-        accessJwt: session.accessJwt,
-        refreshJwt: session.refreshJwt,
-      })
+  /**
+   * Attempts to resume the previous session loaded from storage
+   */
+  async attemptSessionResumption() {
+    const sess = this.currentSession
+    if (sess) {
+      this.rootStore.log.debug(
+        'SessionModel:attemptSessionResumption found stored session',
+      )
+      this.isResumingSession = true
+      try {
+        return await this.resumeSession(sess)
+      } finally {
+        this.isResumingSession = false
+      }
+    } else {
+      this.rootStore.log.debug(
+        'SessionModel:attemptSessionResumption has no session to resume',
+      )
     }
   }
 
   /**
-   * Sets up the XRPC API, must be called before connecting to a service
+   * Sets the active session
    */
-  private configureApi(): boolean {
-    if (!this.data) {
-      return false
+  setActiveSession(agent: AtpAgent, did: string) {
+    this.rootStore.log.debug('SessionModel:setActiveSession')
+    this.data = {
+      service: agent.service.toString(),
+      did,
     }
-
-    try {
-      const serviceUri = new URL(this.data.service)
-      this.rootStore.api.xrpc.uri = serviceUri
-    } catch (e: any) {
-      this.rootStore.log.error(
-        `Invalid service URL: ${this.data.service}. Resetting session.`,
-        e,
-      )
-      this.clear()
-      return false
-    }
-
-    this.rootStore.api.sessionManager.set({
-      refreshJwt: this.data.refreshJwt,
-      accessJwt: this.data.accessJwt,
-    })
-    return true
+    this.rootStore.handleSessionChange(agent)
   }
 
   /**
-   * Upserts the current session into the accounts
+   * Upserts a session into the accounts
    */
-  private addSessionToAccounts() {
-    if (!this.data) {
-      return
-    }
+  private persistSession(
+    service: string,
+    did: string,
+    event: AtpSessionEvent,
+    session?: AtpSessionData,
+    addedInfo?: AdditionalAccountData,
+  ) {
+    this.rootStore.log.debug('SessionModel:persistSession', {
+      service,
+      did,
+      event,
+      hasSession: !!session,
+    })
+
+    // upsert the account in our listing
     const existingAccount = this.accounts.find(
-      acc => acc.service === this.data?.service && acc.did === this.data.did,
+      account => account.service === service && account.did === did,
     )
     const newAccount = {
-      service: this.data.service,
-      refreshJwt: this.data.refreshJwt,
-      accessJwt: this.data.accessJwt,
-      handle: this.data.handle,
-      did: this.data.did,
-      displayName: this.rootStore.me.displayName,
-      aviUrl: this.rootStore.me.avatar,
+      service,
+      did,
+      refreshJwt: session?.refreshJwt,
+      accessJwt: session?.accessJwt,
+      handle: session?.handle || existingAccount?.handle || '',
+      displayName: addedInfo
+        ? addedInfo.displayName
+        : existingAccount?.displayName || '',
+      aviUrl: addedInfo ? addedInfo.aviUrl : existingAccount?.aviUrl || '',
     }
     if (!existingAccount) {
       this.accounts.push(newAccount)
     } else {
-      this.accounts = this.accounts
-        .filter(
-          acc =>
-            !(acc.service === this.data?.service && acc.did === this.data.did),
-        )
-        .concat([newAccount])
+      this.accounts = [
+        newAccount,
+        ...this.accounts.filter(
+          account => !(account.service === service && account.did === did),
+        ),
+      ]
+    }
+
+    // if the session expired, fire an event to let the user know
+    if (event === 'expired') {
+      this.rootStore.handleSessionDrop()
     }
   }
 
   /**
    * Clears any session tokens from the accounts; used on logout.
    */
-  private clearSessionTokensFromAccounts() {
+  private clearSessionTokens() {
+    this.rootStore.log.debug('SessionModel:clearSessionTokens')
     this.accounts = this.accounts.map(acct => ({
       service: acct.service,
       handle: acct.handle,
@@ -186,65 +207,73 @@ export class SessionModel {
   }
 
   /**
-   * Fetches the current session from the service, if possible.
-   * Requires an existing session (.data) to be populated with access tokens.
+   * Fetches additional information about an account on load.
    */
-  async connect(): Promise<boolean> {
-    if (this._connectPromise) {
-      return this._connectPromise
+  private async loadAccountInfo(agent: AtpAgent, did: string) {
+    const res = await agent.api.app.bsky.actor
+      .getProfile({actor: did})
+      .catch(_e => undefined)
+    if (res) {
+      return {
+        dispayName: res.data.displayName,
+        aviUrl: res.data.avatar,
+      }
     }
-    this._connectPromise = this._connect()
-    const res = await this._connectPromise
-    this._connectPromise = undefined
-    return res
   }
 
-  private async _connect(): Promise<boolean> {
-    this.attemptingConnect = true
-    if (!this.configureApi()) {
+  /**
+   * Helper to fetch the accounts config settings from an account.
+   */
+  async describeService(service: string): Promise<ServiceDescription> {
+    const agent = new AtpAgent({service})
+    const res = await agent.api.com.atproto.server.getAccountsConfig({})
+    return res.data
+  }
+
+  /**
+   * Attempt to resume a session that we still have access tokens for.
+   */
+  async resumeSession(account: AccountData): Promise<boolean> {
+    this.rootStore.log.debug('SessionModel:resumeSession')
+    if (!(account.accessJwt && account.refreshJwt && account.service)) {
+      this.rootStore.log.debug(
+        'SessionModel:resumeSession aborted due to lack of access tokens',
+      )
       return false
     }
 
+    const agent = new AtpAgent({
+      service: account.service,
+      persistSession: (evt: AtpSessionEvent, sess?: AtpSessionData) => {
+        this.persistSession(account.service, account.did, evt, sess)
+      },
+    })
+
     try {
-      const sess = await this.rootStore.api.com.atproto.session.get()
-      if (sess.success && this.data && this.data.did === sess.data.did) {
-        this.setOnline(true, false)
-        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()
-          })
-        return true // success
-      }
+      await agent.resumeSession({
+        accessJwt: account.accessJwt,
+        refreshJwt: account.refreshJwt,
+        did: account.did,
+        handle: account.handle,
+      })
+      const addedInfo = await this.loadAccountInfo(agent, account.did)
+      this.persistSession(
+        account.service,
+        account.did,
+        'create',
+        agent.session,
+        addedInfo,
+      )
+      this.rootStore.log.debug('SessionModel:resumeSession succeeded')
     } catch (e: any) {
-      if (isNetworkError(e)) {
-        this.setOnline(false, false) // connection issue
-        return false
-      } else {
-        this.clear() // invalid session cached
-      }
+      this.rootStore.log.debug('SessionModel:resumeSession failed', {
+        error: e.toString(),
+      })
+      return false
     }
 
-    this.setOnline(false, false)
-    return false
-  }
-
-  /**
-   * Helper to fetch the accounts config settings from an account.
-   */
-  async describeService(service: string): Promise<ServiceDescription> {
-    const api = AtpApi.service(service) as SessionServiceClient
-    const res = await api.com.atproto.server.getAccountsConfig({})
-    return res.data
+    this.setActiveSession(agent, account.did)
+    return true
   }
 
   /**
@@ -252,78 +281,32 @@ export class SessionModel {
    */
   async login({
     service,
-    handle,
+    identifier,
     password,
   }: {
     service: string
-    handle: string
+    identifier: string
     password: string
   }) {
-    const api = AtpApi.service(service) as SessionServiceClient
-    const res = await api.com.atproto.session.create({handle, password})
-    if (res.data.accessJwt && res.data.refreshJwt) {
-      this.setState({
-        service: service,
-        accessJwt: res.data.accessJwt,
-        refreshJwt: res.data.refreshJwt,
-        handle: res.data.handle,
-        did: res.data.did,
-      })
-      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()
-        })
-    }
-  }
-
-  /**
-   * Attempt to resume a session that we still have access tokens for.
-   */
-  async resumeSession(account: AccountData): Promise<boolean> {
-    if (!(account.accessJwt && account.refreshJwt && account.service)) {
-      return false
+    this.rootStore.log.debug('SessionModel:login')
+    const agent = new AtpAgent({service})
+    await agent.login({identifier, password})
+    if (!agent.session) {
+      throw new Error('Failed to establish session')
     }
 
-    // test that the session is good
-    const api = AtpApi.service(account.service)
-    api.sessionManager.set({
-      refreshJwt: account.refreshJwt,
-      accessJwt: account.accessJwt,
-    })
-    try {
-      const sess = await api.com.atproto.session.get()
-      if (
-        !sess.success ||
-        sess.data.did !== account.did ||
-        !api.sessionManager.session
-      ) {
-        return false
-      }
+    const did = agent.session.did
+    const addedInfo = await this.loadAccountInfo(agent, did)
 
-      // copy over the access tokens, as they may have refreshed during the .get() above
-      runInAction(() => {
-        account.refreshJwt = api.sessionManager.session?.refreshJwt
-        account.accessJwt = api.sessionManager.session?.accessJwt
-      })
-    } catch (_e) {
-      return false
-    }
+    this.persistSession(service, did, 'create', agent.session, addedInfo)
+    agent.setPersistSessionHandler(
+      (evt: AtpSessionEvent, sess?: AtpSessionData) => {
+        this.persistSession(service, did, evt, sess)
+      },
+    )
 
-    // session is good, connect
-    this.setState({
-      service: account.service,
-      accessJwt: account.accessJwt,
-      refreshJwt: account.refreshJwt,
-      handle: account.handle,
-      did: account.did,
-    })
-    return this.connect()
+    this.setActiveSession(agent, did)
+    this.rootStore.log.debug('SessionModel:login succeeded')
   }
 
   async createAccount({
@@ -339,38 +322,41 @@ export class SessionModel {
     handle: string
     inviteCode?: string
   }) {
-    const api = AtpApi.service(service) as SessionServiceClient
-    const res = await api.com.atproto.account.create({
+    this.rootStore.log.debug('SessionModel:createAccount')
+    const agent = new AtpAgent({service})
+    await agent.createAccount({
       handle,
       password,
       email,
       inviteCode,
     })
-    if (res.data.accessJwt && res.data.refreshJwt) {
-      this.setState({
-        service: service,
-        accessJwt: res.data.accessJwt,
-        refreshJwt: res.data.refreshJwt,
-        handle: res.data.handle,
-        did: res.data.did,
-      })
-      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()
-        })
+    if (!agent.session) {
+      throw new Error('Failed to establish session')
     }
+
+    const did = agent.session.did
+    const addedInfo = await this.loadAccountInfo(agent, did)
+
+    this.persistSession(service, did, 'create', agent.session, addedInfo)
+    agent.setPersistSessionHandler(
+      (evt: AtpSessionEvent, sess?: AtpSessionData) => {
+        this.persistSession(service, did, evt, sess)
+      },
+    )
+
+    this.setActiveSession(agent, did)
+    this.rootStore.onboard.start()
+    this.rootStore.log.debug('SessionModel:createAccount succeeded')
   }
 
   /**
    * Close all sessions across all accounts.
    */
   async logout() {
+    this.rootStore.log.debug('SessionModel:logout')
+    // TODO
+    // need to evaluate why deleting the session has caused errors at times
+    // -prf
     /*if (this.hasSession) {
       this.rootStore.api.com.atproto.session.delete().catch((e: any) => {
         this.rootStore.log.warn(
@@ -379,7 +365,7 @@ export class SessionModel {
         )
       })
     }*/
-    this.clearSessionTokensFromAccounts()
-    this.rootStore.clearAll()
+    this.clearSessionTokens()
+    this.rootStore.clearAllSessionState()
   }
 }