Repository: gitify-app/gitify
Pull request #1139 (2024-05-23 11:19:45)

feat(accounts): enhance auth account data structure
- Introduce new accounts type that unlocks the possibility of authenticating one or more accounts at once, across any combination of support auth methods (personal access token or oauth app) and across any platforms (github cloud or github enterprise server)

- Update logic to support new account structures

- Implemented a legacy auth/account migration process to help seamlessly transition existing authenticated users.

_Note: there is a handful of follow-up PRs required in order to expand functionality beyond "at most one account of each auth method"_

setchy: OK - @brandonweiss @afonsojramos - I feel like this is ready.  

It largely focuses on the new account data structure and legacy account migration logic.
setchy: _👀 looking into the coverage drop_
brandonweiss: @setchy I think you might have tagged the wrong person 😬
bmulholland: Any chance there's more that could be pulled out? Not sure when I'll have time for an 800 line review :)
setchy: It's as lean as it'll get. 

It's not as scary as it may appear 
setchy: @bmulholland - thinking more about this... I _could_ do the following PRs

1. new unused types
* contains the new Account types
* set sensible defaults
* leave unused

2. migration utility
* logic to migrate legacy accounts to the new account structure

3. switch all API calls to use the new account object from 1 above.
setchy: Thanks @afonsojramos - anything explode haha
afonsojramos: @setchy When I opened I didn't have the live one open. And now if I try to open the live one it doesn't open either.
<img width="612" alt="image" src="https://github.com/gitify-app/gitify/assets/19473034/0b81809d-2e73-4275-b3bd-aeba1780dd61">
<img width="1362" alt="image" src="https://github.com/gitify-app/gitify/assets/19473034/0a61af44-986d-431c-a52e-0b305a707647">

afonsojramos: If I open both, "live" one first, then local, the local one show the login screen.
<img width="612" alt="image" src="https://github.com/gitify-app/gitify/assets/19473034/21e4bcc7-130e-49f8-befc-18aa0cb90819">
While the live one stays on the white screen.
setchy: 🥹
setchy: The stacktrace might be unrelated to this PR... similar error reported in https://github.com/gitify-app/gitify/issues/1166
afonsojramos: Looking good!

Patch:
diff --git a/src/__mocks__/notifications-mocks.ts b/src/__mocks__/notifications-mocks.ts
index a4a359f1..438ab37b 100644
--- a/src/__mocks__/notifications-mocks.ts
+++ b/src/__mocks__/notifications-mocks.ts
@@ -2,23 +2,26 @@ import type { AccountNotifications } from '../types';
 import {
   mockEnterpriseNotifications,
   mockGitHubNotifications,
-  mockSingleNotification,
 } from '../utils/api/__mocks__/response-mocks';
+import {
+  mockGitHubCloudAccount,
+  mockGitHubEnterpriseServerAccount,
+} from './state-mocks';
 
 export const mockAccountNotifications: AccountNotifications[] = [
   {
-    hostname: 'github.com',
+    account: mockGitHubCloudAccount,
     notifications: mockGitHubNotifications,
   },
   {
-    hostname: 'github.gitify.io',
+    account: mockGitHubEnterpriseServerAccount,
     notifications: mockEnterpriseNotifications,
   },
 ];
 
 export const mockSingleAccountNotifications: AccountNotifications[] = [
   {
-    hostname: 'github.com',
-    notifications: [mockSingleNotification],
+    account: mockGitHubCloudAccount,
+    notifications: [mockGitHubNotifications[0]],
   },
 ];
diff --git a/src/__mocks__/partial-mocks.ts b/src/__mocks__/partial-mocks.ts
index f0d2c0ef..aefbbe9b 100644
--- a/src/__mocks__/partial-mocks.ts
+++ b/src/__mocks__/partial-mocks.ts
@@ -1,11 +1,18 @@
 import type { Notification, Subject, User } from '../typesGitHub';
 import Constants from '../utils/constants';
+import { mockGitifyUser, mockToken } from './state-mocks';
 
 export function partialMockNotification(
   subject: Partial<Subject>,
 ): Notification {
   const mockNotification: Partial<Notification> = {
-    hostname: Constants.GITHUB_API_BASE_URL,
+    account: {
+      method: 'Personal Access Token',
+      platform: 'GitHub Cloud',
+      hostname: Constants.GITHUB_API_BASE_URL,
+      token: mockToken,
+      user: mockGitifyUser,
+    },
     subject: subject as Subject,
   };
 
diff --git a/src/__mocks__/state-mocks.ts b/src/__mocks__/state-mocks.ts
index 90bf49f2..118e11bb 100644
--- a/src/__mocks__/state-mocks.ts
+++ b/src/__mocks__/state-mocks.ts
@@ -1,10 +1,12 @@
 import {
+  type Account,
   type AuthState,
   type GitifyUser,
   type SettingsState,
   Theme,
 } from '../types';
 import type { EnterpriseAccount } from '../utils/auth/types';
+import Constants from '../utils/constants';
 
 export const mockEnterpriseAccounts: EnterpriseAccount[] = [
   {
@@ -13,18 +15,50 @@ export const mockEnterpriseAccounts: EnterpriseAccount[] = [
   },
 ];
 
-export const mockUser: GitifyUser = {
+export const mockGitifyUser: GitifyUser = {
   login: 'octocat',
   name: 'Mona Lisa Octocat',
   id: 123456789,
 };
 
-export const mockAuth: AuthState = {
+export const mockPersonalAccessTokenAccount: Account = {
+  platform: 'GitHub Cloud',
+  method: 'Personal Access Token',
+  token: 'token-123-456',
+  hostname: Constants.DEFAULT_AUTH_OPTIONS.hostname,
+  user: mockGitifyUser,
+};
+
+export const mockOAuthAccount: Account = {
+  platform: 'GitHub Enterprise Server',
+  method: 'OAuth App',
+  token: '1234568790',
+  hostname: 'github.gitify.io',
+  user: mockGitifyUser,
+};
+
+export const mockGitHubCloudAccount: Account = {
+  platform: 'GitHub Cloud',
+  method: 'Personal Access Token',
   token: 'token-123-456',
-  enterpriseAccounts: mockEnterpriseAccounts,
-  user: mockUser,
+  hostname: Constants.DEFAULT_AUTH_OPTIONS.hostname,
+  user: mockGitifyUser,
 };
 
+export const mockGitHubEnterpriseServerAccount: Account = {
+  platform: 'GitHub Enterprise Server',
+  method: 'Personal Access Token',
+  token: '1234568790',
+  hostname: 'github.gitify.io',
+  user: mockGitifyUser,
+};
+
+export const mockAuth: AuthState = {
+  accounts: [mockGitHubCloudAccount, mockGitHubEnterpriseServerAccount],
+};
+
+export const mockToken = 'token-123-456';
+
 export const mockSettings: SettingsState = {
   participating: false,
   playSound: true,
diff --git a/src/components/NotificationRow.tsx b/src/components/NotificationRow.tsx
index b322f0fe..a57c0855 100644
--- a/src/components/NotificationRow.tsx
+++ b/src/components/NotificationRow.tsx
@@ -36,8 +36,8 @@ interface IProps {
 
 export const NotificationRow: FC<IProps> = ({ notification, hostname }) => {
   const {
-    settings,
     auth,
+    settings,
     removeNotificationFromState,
     markNotificationRead,
     markNotificationDone,
@@ -46,7 +46,7 @@ export const NotificationRow: FC<IProps> = ({ notification, hostname }) => {
   } = useContext(AppContext);
 
   const openNotification = useCallback(() => {
-    openInBrowser(notification, auth);
+    openInBrowser(notification);
 
     if (settings.markAsDoneOnOpen) {
       markNotificationDone(notification.id, hostname);
diff --git a/src/context/App.tsx b/src/context/App.tsx
index b29edebe..b84c8b3e 100644
--- a/src/context/App.tsx
+++ b/src/context/App.tsx
@@ -17,6 +17,7 @@ import {
   Theme,
 } from '../types';
 import { headNotifications } from '../utils/api/client';
+import { migrateAuthenticatedAccounts } from '../utils/auth/migration';
 import type {
   LoginOAuthAppOptions,
   LoginPersonalAccessTokenOptions,
@@ -34,6 +35,7 @@ import { clearState, loadState, saveState } from '../utils/storage';
 import { setTheme } from '../utils/theme';
 
 const defaultAuth: AuthState = {
+  accounts: [],
   token: null,
   enterpriseAccounts: [],
   user: null,
@@ -116,8 +118,7 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
     settings.participating,
     settings.showBots,
     settings.detailedNotifications,
-    auth.token,
-    auth.enterpriseAccounts.length,
+    auth.accounts.length,
   ]);
 
   useInterval(() => {
@@ -149,7 +150,7 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
   );
 
   const isLoggedIn = useMemo(() => {
-    return !!auth.token || auth.enterpriseAccounts.length > 0;
+    return auth.accounts.length > 0;
   }, [auth]);
 
   const loginWithGitHubApp = useCallback(async () => {
@@ -157,7 +158,7 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
     const { token } = await getToken(authCode);
     const hostname = Constants.DEFAULT_AUTH_OPTIONS.hostname;
     const user = await getUserData(token, hostname);
-    const updatedAuth = addAccount(auth, token, hostname, user);
+    const updatedAuth = addAccount(auth, 'GitHub App', token, hostname, user);
     setAuth(updatedAuth);
     saveState({ auth: updatedAuth, settings });
   }, [auth, settings]);
@@ -166,7 +167,7 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
     async (data: LoginOAuthAppOptions) => {
       const { authOptions, authCode } = await authGitHub(data);
       const { token, hostname } = await getToken(authCode, authOptions);
-      const updatedAuth = addAccount(auth, token, hostname);
+      const updatedAuth = addAccount(auth, 'OAuth App', token, hostname);
       setAuth(updatedAuth);
       saveState({ auth: updatedAuth, settings });
     },
@@ -178,7 +179,13 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
       await headNotifications(hostname, token);
 
       const user = await getUserData(token, hostname);
-      const updatedAuth = addAccount(auth, token, hostname, user);
+      const updatedAuth = addAccount(
+        auth,
+        'Personal Access Token',
+        token,
+        hostname,
+        user,
+      );
       setAuth(updatedAuth);
       saveState({ auth: updatedAuth, settings });
     },
@@ -190,7 +197,9 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
     clearState();
   }, []);
 
-  const restoreSettings = useCallback(() => {
+  const restoreSettings = useCallback(async () => {
+    await migrateAuthenticatedAccounts();
+
     const existing = loadState();
 
     if (existing.auth) {
diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts
index a048338a..337ffc2b 100644
--- a/src/hooks/useNotifications.ts
+++ b/src/hooks/useNotifications.ts
@@ -13,7 +13,7 @@ import {
   markRepositoryNotificationsAsRead,
 } from '../utils/api/client';
 import { determineFailureType } from '../utils/api/errors';
-import { getTokenForHost } from '../utils/helpers';
+import { getAccountForHost } from '../utils/helpers';
 import {
   getAllNotifications,
   setTrayIconColor,
@@ -107,10 +107,10 @@ export const useNotifications = (): NotificationsState => {
     ) => {
       setStatus('loading');
 
-      const token = getTokenForHost(hostname, auth);
+      const account = getAccountForHost(hostname, auth);
 
       try {
-        await markNotificationThreadAsRead(id, hostname, token);
+        await markNotificationThreadAsRead(id, hostname, account.token);
 
         const updatedNotifications = removeNotification(
           settings,
@@ -138,10 +138,10 @@ export const useNotifications = (): NotificationsState => {
     ) => {
       setStatus('loading');
 
-      const token = getTokenForHost(hostname, auth);
+      const account = getAccountForHost(hostname, auth);
 
       try {
-        await markNotificationThreadAsDone(id, hostname, token);
+        await markNotificationThreadAsDone(id, hostname, account.token);
 
         const updatedNotifications = removeNotification(
           settings,
@@ -169,10 +169,10 @@ export const useNotifications = (): NotificationsState => {
     ) => {
       setStatus('loading');
 
-      const token = getTokenForHost(hostname, auth);
+      const account = getAccountForHost(hostname, auth);
 
       try {
-        await ignoreNotificationThreadSubscription(id, hostname, token);
+        await ignoreNotificationThreadSubscription(id, hostname, account.token);
         await markNotificationRead(auth, settings, id, hostname);
         setStatus('success');
       } catch (err) {
@@ -191,10 +191,14 @@ export const useNotifications = (): NotificationsState => {
     ) => {
       setStatus('loading');
 
-      const token = getTokenForHost(hostname, auth);
+      const account = getAccountForHost(hostname, auth);
 
       try {
-        await markRepositoryNotificationsAsRead(repoSlug, hostname, token);
+        await markRepositoryNotificationsAsRead(
+          repoSlug,
+          hostname,
+          account.token,
+        );
         const updatedNotifications = removeNotifications(
           repoSlug,
           notifications,
@@ -222,7 +226,8 @@ export const useNotifications = (): NotificationsState => {
 
       try {
         const accountIndex = notifications.findIndex(
-          (accountNotifications) => accountNotifications.hostname === hostname,
+          (accountNotifications) =>
+            accountNotifications.account.hostname === hostname,
         );
 
         if (accountIndex !== -1) {
@@ -238,7 +243,7 @@ export const useNotifications = (): NotificationsState => {
                 auth,
                 settings,
                 notification.id,
-                notifications[accountIndex].hostname,
+                notifications[accountIndex].account.hostname,
               ),
             ),
           );
diff --git a/src/routes/LoginWithOAuthApp.tsx b/src/routes/LoginWithOAuthApp.tsx
index f95ff345..63acee45 100644
--- a/src/routes/LoginWithOAuthApp.tsx
+++ b/src/routes/LoginWithOAuthApp.tsx
@@ -4,8 +4,7 @@ import {
   PersonIcon,
   SignInIcon,
 } from '@primer/octicons-react';
-import ipcRenderer from 'electron';
-import { type FC, useCallback, useContext, useEffect } from 'react';
+import { type FC, useCallback, useContext } from 'react';
 import { Form, type FormRenderProps } from 'react-final-form';
 import { useNavigate } from 'react-router-dom';
 import { Button } from '../components/fields/Button';
@@ -57,19 +56,9 @@ export const validate = (values: IValues): IFormErrors => {
 };
 
 export const LoginWithOAuthApp: FC = () => {
-  const {
-    auth: { enterpriseAccounts },
-    loginWithOAuthApp: loginEnterprise,
-  } = useContext(AppContext);
+  const { loginWithOAuthApp } = useContext(AppContext);
   const navigate = useNavigate();
 
-  useEffect(() => {
-    if (enterpriseAccounts.length) {
-      ipcRenderer.ipcRenderer.send('reopen-window');
-      navigate(-1);
-    }
-  }, [enterpriseAccounts]);
-
   const renderForm = (formProps: FormRenderProps) => {
     const { handleSubmit, submitting, pristine, values } = formProps;
 
@@ -129,7 +118,7 @@ export const LoginWithOAuthApp: FC = () => {
 
   const login = useCallback(async (data: IValues) => {
     try {
-      await loginEnterprise(data as LoginOAuthAppOptions);
+      await loginWithOAuthApp(data as LoginOAuthAppOptions);
     } catch (err) {
       // Skip
     }
diff --git a/src/routes/Notifications.tsx b/src/routes/Notifications.tsx
index b8b5f83d..626d938d 100644
--- a/src/routes/Notifications.tsx
+++ b/src/routes/Notifications.tsx
@@ -33,11 +33,11 @@ export const NotificationsRoute: FC = () => {
 
   return (
     <div className="flex flex-col flex-1 bg-white dark:bg-gray-dark">
-      {notifications.map((account) => (
+      {notifications.map((accountNotifications) => (
         <AccountNotifications
-          key={account.hostname}
-          hostname={account.hostname}
-          notifications={account.notifications}
+          key={accountNotifications.account.hostname}
+          hostname={accountNotifications.account.hostname}
+          notifications={accountNotifications.notifications}
           showAccountHostname={
             hasMultipleAccounts || settings.showAccountHostname
           }
diff --git a/src/types.ts b/src/types.ts
index 66d593e6..fcdcbed3 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,15 +1,37 @@
 import type { OcticonProps } from '@primer/octicons-react';
 import type { FC } from 'react';
 import type { Notification } from './typesGitHub';
-import type { EnterpriseAccount } from './utils/auth/types';
+import type {
+  AuthMethod,
+  EnterpriseAccount,
+  PlatformType,
+} from './utils/auth/types';
+
+export type Status = 'loading' | 'success' | 'error';
 
 export interface AuthState {
+  accounts: Account[];
+  /**
+   * @deprecated This attribute is deprecated and will be removed in a future release.
+   */
   token?: string;
-  enterpriseAccounts: EnterpriseAccount[];
-  user: GitifyUser | null;
+  /**
+   * @deprecated This attribute is deprecated and will be removed in a future release.
+   */
+  enterpriseAccounts?: EnterpriseAccount[];
+  /**
+   * @deprecated This attribute is deprecated and will be removed in a future release.
+   */
+  user?: GitifyUser | null;
 }
 
-export type Status = 'loading' | 'success' | 'error';
+export interface Account {
+  method: AuthMethod;
+  platform: PlatformType;
+  hostname: string;
+  token: string;
+  user: GitifyUser | null;
+}
 
 export type SettingsState = AppearanceSettingsState &
   NotificationSettingsState &
@@ -52,7 +74,7 @@ export type RadioGroupItem = {
 };
 
 export interface AccountNotifications {
-  hostname: string;
+  account: Account;
   notifications: Notification[];
 }
 
diff --git a/src/typesGitHub.ts b/src/typesGitHub.ts
index ea86008b..416b0164 100644
--- a/src/typesGitHub.ts
+++ b/src/typesGitHub.ts
@@ -1,3 +1,5 @@
+import type { Account } from './types';
+
 export type Reason =
   | 'approval_requested'
   | 'assign'
@@ -91,7 +93,9 @@ export type PullRequestReviewAuthorAssociation =
   | 'NONE'
   | 'OWNER';
 
-export interface Notification {
+export type Notification = GitHubNotification & GitifyNotification;
+
+export interface GitHubNotification {
   id: string;
   unread: boolean;
   reason: Reason;
@@ -101,8 +105,11 @@ export interface Notification {
   repository: Repository;
   url: string;
   subscription_url: string;
-  // TODO - rename this to apiBaseUrl
-  hostname: string; // This is not in the official GitHub API. We add this to make notification interactions easier.
+}
+
+// Note: This is not in the official GitHub API. We add this to make notification interactions easier.
+export interface GitifyNotification {
+  account: Account;
 }
 
 export type UserDetails = User & UserProfile;
diff --git a/src/utils/api/__mocks__/response-mocks.ts b/src/utils/api/__mocks__/response-mocks.ts
index dcaf441b..603a58fb 100644
--- a/src/utils/api/__mocks__/response-mocks.ts
+++ b/src/utils/api/__mocks__/response-mocks.ts
@@ -1,3 +1,7 @@
+import {
+  mockGitHubCloudAccount,
+  mockGitHubEnterpriseServerAccount,
+} from '../../../__mocks__/state-mocks';
 import type {
   Discussion,
   DiscussionAuthor,
@@ -7,7 +11,6 @@ import type {
   Repository,
   User,
 } from '../../../typesGitHub';
-import Constants from '../../constants';
 
 export const mockNotificationUser: User = {
   login: 'octocat',
@@ -35,7 +38,7 @@ export const mockNotificationUser: User = {
 // Repository : 'gitify-app/notifications-test'
 export const mockGitHubNotifications: Notification[] = [
   {
-    hostname: Constants.GITHUB_API_BASE_URL,
+    account: mockGitHubCloudAccount,
     id: '138661096',
     unread: true,
     reason: 'subscribed',
@@ -183,7 +186,7 @@ export const mockGitHubNotifications: Notification[] = [
       'https://api.github.com/notifications/threads/138661096/subscription',
   },
   {
-    hostname: Constants.GITHUB_API_BASE_URL,
+    account: mockGitHubCloudAccount,
     id: '148827438',
     unread: true,
     reason: 'author',
@@ -242,7 +245,7 @@ export const mockGitHubNotifications: Notification[] = [
 // Repository : 'myorg/notifications-test'
 export const mockEnterpriseNotifications: Notification[] = [
   {
-    hostname: 'https://github.gitify.io/api/v3',
+    account: mockGitHubEnterpriseServerAccount,
     id: '3',
     unread: true,
     reason: 'subscribed',
@@ -296,7 +299,7 @@ export const mockEnterpriseNotifications: Notification[] = [
       'https://github.gitify.io/api/v3/notifications/threads/4/subscription',
   },
   {
-    hostname: 'https://github.gitify.io/api/v3',
+    account: mockGitHubEnterpriseServerAccount,
     id: '4',
     unread: true,
     reason: 'subscribed',
diff --git a/src/utils/api/client.ts b/src/utils/api/client.ts
index ae9549a6..3c74a8bd 100644
--- a/src/utils/api/client.ts
+++ b/src/utils/api/client.ts
@@ -1,6 +1,6 @@
 import type { AxiosPromise } from 'axios';
 import { print } from 'graphql/language/printer';
-import type { SettingsState } from '../../types';
+import type { Account, SettingsState } from '../../types';
 import type {
   Commit,
   CommitComment,
@@ -52,15 +52,14 @@ export function headNotifications(
  * Endpoint documentation: https://docs.github.com/en/rest/activity/notifications#list-notifications-for-the-authenticated-user
  */
 export function listNotificationsForAuthenticatedUser(
-  hostname: string,
-  token: string,
+  account: Account,
   settings: SettingsState,
 ): AxiosPromise<Notification[]> {
-  const url = getGitHubAPIBaseUrl(hostname);
+  const url = getGitHubAPIBaseUrl(account.hostname);
   url.pathname += 'notifications';
   url.searchParams.append('participating', String(settings.participating));
 
-  return apiRequestAuth(url.toString(), 'GET', token);
+  return apiRequestAuth(url.toString(), 'GET', account.token);
 }
 
 /**
@@ -229,10 +228,9 @@ export async function getHtmlUrl(url: string, token: string): Promise<string> {
  */
 export async function searchDiscussions(
   notification: Notification,
-  token: string,
 ): AxiosPromise<GraphQLSearch<Discussion>> {
-  const url = getGitHubGraphQLUrl(notification.hostname);
-  return apiRequestAuth(url.toString(), 'POST', token, {
+  const url = getGitHubGraphQLUrl(notification.account.hostname);
+  return apiRequestAuth(url.toString(), 'POST', notification.account.token, {
     query: print(QUERY_SEARCH_DISCUSSIONS),
     variables: {
       queryStatement: formatAsGitHubSearchSyntax(
@@ -251,10 +249,9 @@ export async function searchDiscussions(
  */
 export async function getLatestDiscussion(
   notification: Notification,
-  token: string,
 ): Promise<Discussion | null> {
   try {
-    const response = await searchDiscussions(notification, token);
+    const response = await searchDiscussions(notification);
     return (
       response.data?.data.search.nodes.filter(
         (discussion) => discussion.title === notification.subject.title,
diff --git a/src/utils/auth/migration.ts b/src/utils/auth/migration.ts
new file mode 100644
index 00000000..d8afe6e9
--- /dev/null
+++ b/src/utils/auth/migration.ts
@@ -0,0 +1,85 @@
+import type { Account, AuthState } from '../../types';
+import Constants from '../constants';
+import { loadState, saveState } from '../storage';
+import { getUserData } from './utils';
+
+/**
+ * Migrate authenticated accounts from old data structure to new data structure (v5.7.0+).
+ *
+ * @deprecated We plan to remove this migration logic in a future major release.
+ */
+export async function migrateAuthenticatedAccounts() {
+  const existing = loadState();
+
+  if (hasAccountsToMigrate(existing.auth)) {
+    console.log('Commencing authenticated accounts migration');
+
+    const migratedAccounts = await convertAccounts(existing.auth);
+
+    saveState({
+      auth: { ...existing.auth, accounts: migratedAccounts },
+      settings: existing.settings,
+    });
+    console.log('Authenticated accounts migration complete');
+  }
+}
+
+export function hasAccountsToMigrate(existingAuthState: AuthState): boolean {
+  if (!existingAuthState) {
+    return false;
+  }
+
+  // Don't attempt migration if there are already accounts in the new structure
+  if (existingAuthState?.accounts?.length > 0) {
+    return false;
+  }
+
+  if (
+    existingAuthState?.token ||
+    existingAuthState?.enterpriseAccounts?.length > 0
+  ) {
+    return true;
+  }
+
+  return false;
+}
+
+export async function convertAccounts(
+  existingAuthState: AuthState,
+): Promise<Account[]> {
+  const migratedAccounts: Account[] = [];
+
+  if (existingAuthState?.token) {
+    const user = await getUserData(
+      existingAuthState.token,
+      Constants.DEFAULT_AUTH_OPTIONS.hostname,
+    );
+
+    migratedAccounts.push({
+      hostname: Constants.DEFAULT_AUTH_OPTIONS.hostname,
+      platform: 'GitHub Cloud',
+      method: 'Personal Access Token',
+      token: existingAuthState.token,
+      user: user,
+    });
+  }
+
+  if (existingAuthState?.enterpriseAccounts) {
+    for (const legacyEnterpriseAccount of existingAuthState.enterpriseAccounts) {
+      const user = await getUserData(
+        legacyEnterpriseAccount.token,
+        legacyEnterpriseAccount.hostname,
+      );
+
+      migratedAccounts.push({
+        hostname: legacyEnterpriseAccount.hostname,
+        platform: 'GitHub Enterprise Server',
+        method: 'OAuth App',
+        token: legacyEnterpriseAccount.token,
+        user: user,
+      });
+    }
+  }
+
+  return migratedAccounts;
+}
diff --git a/src/utils/auth/types.ts b/src/utils/auth/types.ts
index d2b72b7f..f51261d8 100644
--- a/src/utils/auth/types.ts
+++ b/src/utils/auth/types.ts
@@ -1,3 +1,7 @@
+export type AuthMethod = 'GitHub App' | 'Personal Access Token' | 'OAuth App';
+
+export type PlatformType = 'GitHub Cloud' | 'GitHub Enterprise Server';
+
 export interface LoginOAuthAppOptions {
   hostname: string;
   clientId: string;
@@ -19,6 +23,9 @@ export interface AuthTokenResponse {
   token: string;
 }
 
+/**
+ * @deprecated This type is deprecated and will be removed in a future release.
+ */
 export interface EnterpriseAccount {
   hostname: string;
   token: string;
diff --git a/src/utils/auth/utils.ts b/src/utils/auth/utils.ts
index c09dab27..05a80e95 100644
--- a/src/utils/auth/utils.ts
+++ b/src/utils/auth/utils.ts
@@ -5,8 +5,8 @@ import type { UserDetails } from '../../typesGitHub';
 import { getAuthenticatedUser } from '../api/client';
 import { apiRequest } from '../api/request';
 import { Constants } from '../constants';
-import { isEnterpriseHost } from '../helpers';
-import type { AuthResponse, AuthTokenResponse } from './types';
+import { getPlatformFromHostname } from '../helpers';
+import type { AuthMethod, AuthResponse, AuthTokenResponse } from './types';
 
 export const authGitHub = (
   authOptions = Constants.DEFAULT_AUTH_OPTIONS,
@@ -107,31 +107,26 @@ export const getToken = async (
   };
 };
 
-export const addAccount = (
+export function addAccount(
   auth: AuthState,
-  token,
-  hostname,
+  method: AuthMethod,
+  token: string,
+  hostname: string,
   user?: GitifyUser,
-): AuthState => {
-  if (!isEnterpriseHost(hostname)) {
-    return {
-      ...auth,
-      token,
-      user: user ?? null,
-    };
-  }
-
+): AuthState {
   return {
-    ...auth,
-    enterpriseAccounts: [
-      ...auth.enterpriseAccounts,
+    accounts: [
+      ...auth.accounts,
       {
-        token,
         hostname: hostname,
+        method: method,
+        platform: getPlatformFromHostname(hostname),
+        token: token,
+        user: user,
       },
     ],
   };
-};
+}
 
 export function getNewTokenURL(hostname: string): string {
   const date = format(new Date(), 'PP p');
diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
index 4548c02f..f175c8b4 100644
--- a/src/utils/helpers.ts
+++ b/src/utils/helpers.ts
@@ -1,8 +1,9 @@
 import { formatDistanceToNow, parseISO } from 'date-fns';
-import type { AuthState } from '../types';
+import type { Account, AuthState } from '../types';
 import type { Notification } from '../typesGitHub';
 import { openExternalLink } from '../utils/comms';
 import { getHtmlUrl, getLatestDiscussion } from './api/client';
+import type { PlatformType } from './auth/types';
 import { Constants } from './constants';
 import {
   getCheckSuiteAttributes,
@@ -11,23 +12,23 @@ import {
 } from './subject';
 
 export function isPersonalAccessTokenLoggedIn(auth: AuthState): boolean {
-  return auth.token != null;
+  return auth.accounts.some(
+    (account) => account.method === 'Personal Access Token',
+  );
 }
 
 export function isOAuthAppLoggedIn(auth: AuthState): boolean {
-  return auth.enterpriseAccounts?.length > 0;
+  return auth.accounts.some((account) => account.method === 'OAuth App');
 }
 
-export function getTokenForHost(hostname: string, auth: AuthState): string {
-  const isEnterprise = isEnterpriseHost(hostname);
-
-  if (isEnterprise) {
-    return auth.enterpriseAccounts.find(
-      (account) => account.hostname === hostname,
-    ).token;
-  }
+export function getAccountForHost(hostname: string, auth: AuthState): Account {
+  return auth.accounts.find((account) => hostname.endsWith(account.hostname));
+}
 
-  return auth.token;
+export function getPlatformFromHostname(hostname: string): PlatformType {
+  return hostname.endsWith(Constants.DEFAULT_AUTH_OPTIONS.hostname)
+    ? 'GitHub Cloud'
+    : 'GitHub Enterprise Server';
 }
 
 export function isEnterpriseHost(hostname: string): boolean {
@@ -35,11 +36,10 @@ export function isEnterpriseHost(hostname: string): boolean {
 }
 
 export function generateNotificationReferrerId(
-  notificationId: string,
-  userId: number,
+  notification: Notification,
 ): string {
   const buffer = Buffer.from(
-    `018:NotificationThread${notificationId}:${userId}`,
+    `018:NotificationThread${notification.id}:${notification.account.user.id}`,
   );
   return buffer.toString('base64');
 }
@@ -93,14 +93,11 @@ export function actionsURL(repositoryURL: string, filters: string[]): string {
   return url.toString().replace(/%2B/g, '+');
 }
 
-async function getDiscussionUrl(
-  notification: Notification,
-  token: string,
-): Promise<string> {
+async function getDiscussionUrl(notification: Notification): Promise<string> {
   const url = new URL(notification.repository.html_url);
   url.pathname += '/discussions';
 
-  const discussion = await getLatestDiscussion(notification, token);
+  const discussion = await getLatestDiscussion(notification);
 
   if (discussion) {
     url.href = discussion.url;
@@ -117,15 +114,19 @@ async function getDiscussionUrl(
 
 export async function generateGitHubWebUrl(
   notification: Notification,
-  auth: AuthState,
 ): Promise<string> {
   const url = new URL(notification.repository.html_url);
-  const token = getTokenForHost(notification.hostname, auth);
 
   if (notification.subject.latest_comment_url) {
-    url.href = await getHtmlUrl(notification.subject.latest_comment_url, token);
+    url.href = await getHtmlUrl(
+      notification.subject.latest_comment_url,
+      notification.account.token,
+    );
   } else if (notification.subject.url) {
-    url.href = await getHtmlUrl(notification.subject.url, token);
+    url.href = await getHtmlUrl(
+      notification.subject.url,
+      notification.account.token,
+    );
   } else {
     // Perform any specific notification type handling (only required for a few special notification scenarios)
     switch (notification.subject.type) {
@@ -133,7 +134,7 @@ export async function generateGitHubWebUrl(
         url.href = getCheckSuiteUrl(notification);
         break;
       case 'Discussion':
-        url.href = await getDiscussionUrl(notification, token);
+        url.href = await getDiscussionUrl(notification);
         break;
       case 'RepositoryInvitation':
         url.pathname += '/invitations';
@@ -148,7 +149,7 @@ export async function generateGitHubWebUrl(
 
   url.searchParams.set(
     'notification_referrer_id',
-    generateNotificationReferrerId(notification.id, auth.user?.id),
+    generateNotificationReferrerId(notification),
   );
 
   return url.toString();
@@ -183,11 +184,8 @@ export function formatNotificationUpdatedAt(
   return '';
 }
 
-export async function openInBrowser(
-  notification: Notification,
-  auth: AuthState,
-) {
-  const url = await generateGitHubWebUrl(notification, auth);
+export async function openInBrowser(notification: Notification) {
+  const url = await generateGitHubWebUrl(notification);
 
   openExternalLink(url);
 }
diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts
index d44bcc6e..33695a4a 100644
--- a/src/utils/notifications.ts
+++ b/src/utils/notifications.ts
@@ -1,14 +1,9 @@
 import { ipcRenderer } from 'electron';
 import type { AccountNotifications, AuthState, SettingsState } from '../types';
 import { Notification } from '../typesGitHub';
-import {
-  getTokenForHost,
-  isPersonalAccessTokenLoggedIn,
-  openInBrowser,
-} from '../utils/helpers';
+import { openInBrowser } from '../utils/helpers';
 import { listNotificationsForAuthenticatedUser } from './api/client';
 import { updateTrayIcon } from './comms';
-import Constants from './constants';
 import { isWindows } from './platform';
 import { getGitifySubjectDetails } from './subject';
 
@@ -32,21 +27,24 @@ export const triggerNativeNotifications = (
   auth: AuthState,
 ) => {
   const diffNotifications = newNotifications
-    .map((account) => {
+    .map((accountNotifications) => {
       const accountPreviousNotifications = previousNotifications.find(
-        (item) => item.hostname === account.hostname,
+        (item) =>
+          item.account.hostname === accountNotifications.account.hostname,
       );
 
       if (!accountPreviousNotifications) {
-        return account.notifications;
+        return accountNotifications.notifications;
       }
 
       const accountPreviousNotificationsIds =
         accountPreviousNotifications.notifications.map((item) => item.id);
 
-      const accountNewNotifications = account.notifications.filter((item) => {
-        return !accountPreviousNotificationsIds.includes(`${item.id}`);
-      });
+      const accountNewNotifications = accountNotifications.notifications.filter(
+        (item) => {
+          return !accountPreviousNotificationsIds.includes(`${item.id}`);
+        },
+      );
 
       return accountNewNotifications;
     })
@@ -94,7 +92,7 @@ export const raiseNativeNotification = (
   nativeNotification.onclick = () => {
     if (notifications.length === 1) {
       ipcRenderer.send('hide-window');
-      openInBrowser(notifications[0], auth);
+      openInBrowser(notifications[0]);
     } else {
       ipcRenderer.send('reopen-window');
     }
@@ -109,25 +107,12 @@ export const raiseSoundNotification = () => {
   audio.play();
 };
 
-function getGitHubNotifications(auth: AuthState, settings: SettingsState) {
-  if (!isPersonalAccessTokenLoggedIn(auth)) {
-    return;
-  }
-
-  return listNotificationsForAuthenticatedUser(
-    Constants.DEFAULT_AUTH_OPTIONS.hostname,
-    auth.token,
-    settings,
-  );
-}
-
-function getEnterpriseNotifications(auth: AuthState, settings: SettingsState) {
-  return auth.enterpriseAccounts.map((account) => {
-    return listNotificationsForAuthenticatedUser(
-      account.hostname,
-      account.token,
-      settings,
-    );
+function getNotifications(auth: AuthState, settings: SettingsState) {
+  return auth.accounts.map((account) => {
+    return {
+      account,
+      notifications: listNotificationsForAuthenticatedUser(account, settings),
+    };
   });
 }
 
@@ -135,21 +120,16 @@ export async function getAllNotifications(
   auth: AuthState,
   settings: SettingsState,
 ): Promise<AccountNotifications[]> {
-  const responses = await Promise.all([
-    getGitHubNotifications(auth, settings),
-    ...getEnterpriseNotifications(auth, settings),
-  ]);
+  const responses = await Promise.all([...getNotifications(auth, settings)]);
 
   const notifications: AccountNotifications[] = await Promise.all(
     responses
       .filter((response) => !!response)
       .map(async (accountNotifications) => {
-        const { hostname } = new URL(accountNotifications.config.url);
-
-        let notifications = accountNotifications.data.map(
+        let notifications = (await accountNotifications.notifications).data.map(
           (notification: Notification) => ({
             ...notification,
-            hostname,
+            account: accountNotifications.account,
           }),
         );
 
@@ -162,7 +142,7 @@ export async function getAllNotifications(
         notifications = filterNotifications(notifications, settings);
 
         return {
-          hostname,
+          account: accountNotifications.account,
           notifications: notifications,
         };
       }),
@@ -182,12 +162,8 @@ export async function enrichNotifications(
 
   const enrichedNotifications = await Promise.all(
     notifications.map(async (notification: Notification) => {
-      const token = getTokenForHost(notification.hostname, auth);
-
-      const additionalSubjectDetails = await getGitifySubjectDetails(
-        notification,
-        token,
-      );
+      const additionalSubjectDetails =
+        await getGitifySubjectDetails(notification);
 
       return {
         ...notification,
diff --git a/src/utils/remove-notification.ts b/src/utils/remove-notification.ts
index 4e93cbe5..03e0fcd1 100644
--- a/src/utils/remove-notification.ts
+++ b/src/utils/remove-notification.ts
@@ -14,7 +14,8 @@ export const removeNotification = (
   }
 
   const accountIndex = notifications.findIndex(
-    (accountNotifications) => accountNotifications.hostname === hostname,
+    (accountNotifications) =>
+      accountNotifications.account.hostname === hostname,
   );
 
   if (accountIndex !== -1) {
diff --git a/src/utils/remove-notifications.ts b/src/utils/remove-notifications.ts
index 2831e4f6..09ed0728 100644
--- a/src/utils/remove-notifications.ts
+++ b/src/utils/remove-notifications.ts
@@ -6,7 +6,8 @@ export const removeNotifications = (
   hostname: string,
 ): AccountNotifications[] => {
   const accountIndex = notifications.findIndex(
-    (accountNotifications) => accountNotifications.hostname === hostname,
+    (accountNotifications) =>
+      accountNotifications.account.hostname === hostname,
   );
 
   if (accountIndex !== -1) {
diff --git a/src/utils/subject.ts b/src/utils/subject.ts
index d899e7ea..ffcbe2c0 100644
--- a/src/utils/subject.ts
+++ b/src/utils/subject.ts
@@ -25,22 +25,21 @@ import {
 
 export async function getGitifySubjectDetails(
   notification: Notification,
-  token: string,
 ): Promise<GitifySubject> {
   try {
     switch (notification.subject.type) {
       case 'CheckSuite':
         return getGitifySubjectForCheckSuite(notification);
       case 'Commit':
-        return getGitifySubjectForCommit(notification, token);
+        return getGitifySubjectForCommit(notification);
       case 'Discussion':
-        return await getGitifySubjectForDiscussion(notification, token);
+        return await getGitifySubjectForDiscussion(notification);
       case 'Issue':
-        return await getGitifySubjectForIssue(notification, token);
+        return await getGitifySubjectForIssue(notification);
       case 'PullRequest':
-        return await getGitifySubjectForPullRequest(notification, token);
+        return await getGitifySubjectForPullRequest(notification);
       case 'Release':
-        return await getGitifySubjectForRelease(notification, token);
+        return await getGitifySubjectForRelease(notification);
       case 'WorkflowRun':
         return getGitifySubjectForWorkflowRun(notification);
       default:
@@ -115,18 +114,22 @@ function getGitifySubjectForCheckSuite(
 
 async function getGitifySubjectForCommit(
   notification: Notification,
-  token: string,
 ): Promise<GitifySubject> {
   let user: User;
 
   if (notification.subject.latest_comment_url) {
     const commitComment = (
-      await getCommitComment(notification.subject.latest_comment_url, token)
+      await getCommitComment(
+        notification.subject.latest_comment_url,
+        notification.account.token,
+      )
     ).data;
 
     user = commitComment.user;
   } else {
-    const commit = (await getCommit(notification.subject.url, token)).data;
+    const commit = (
+      await getCommit(notification.subject.url, notification.account.token)
+    ).data;
 
     user = commit.author;
   }
@@ -144,9 +147,8 @@ async function getGitifySubjectForCommit(
 
 async function getGitifySubjectForDiscussion(
   notification: Notification,
-  token: string,
 ): Promise<GitifySubject> {
-  const discussion = await getLatestDiscussion(notification, token);
+  const discussion = await getLatestDiscussion(notification);
   let discussionState: DiscussionStateType = 'OPEN';
 
   if (discussion) {
@@ -203,9 +205,10 @@ export function getLatestDiscussionComment(
 
 async function getGitifySubjectForIssue(
   notification: Notification,
-  token: string,
 ): Promise<GitifySubject> {
-  const issue = (await getIssue(notification.subject.url, token)).data;
+  const issue = (
+    await getIssue(notification.subject.url, notification.account.token)
+  ).data;
 
   let issueCommentUser: User;
 
@@ -213,7 +216,7 @@ async function getGitifySubjectForIssue(
     const issueComment = (
       await getIssueOrPullRequestComment(
         notification.subject.latest_comment_url,
-        token,
+        notification.account.token,
       )
     ).data;
     issueCommentUser = issueComment.user;
@@ -233,9 +236,10 @@ async function getGitifySubjectForIssue(
 
 async function getGitifySubjectForPullRequest(
   notification: Notification,
-  token: string,
 ): Promise<GitifySubject> {
-  const pr = (await getPullRequest(notification.subject.url, token)).data;
+  const pr = (
+    await getPullRequest(notification.subject.url, notification.account.token)
+  ).data;
 
   let prState: PullRequestStateType = pr.state;
   if (pr.merged) {
@@ -253,13 +257,13 @@ async function getGitifySubjectForPullRequest(
     const prComment = (
       await getIssueOrPullRequestComment(
         notification.subject.latest_comment_url,
-        token,
+        notification.account.token,
       )
     ).data;
     prCommentUser = prComment.user;
   }
 
-  const reviews = await getLatestReviewForReviewers(notification, token);
+  const reviews = await getLatestReviewForReviewers(notification);
 
   return {
     state: prState,
@@ -276,7 +280,6 @@ async function getGitifySubjectForPullRequest(
 
 export async function getLatestReviewForReviewers(
   notification: Notification,
-  token: string,
 ): Promise<GitifyPullRequestReview[]> | null {
   if (notification.subject.type !== 'PullRequest') {
     return null;
@@ -284,7 +287,7 @@ export async function getLatestReviewForReviewers(
 
   const prReviews = await getPullRequestReviews(
     `${notification.subject.url}/reviews`,
-    token,
+    notification.account.token,
   );
 
   if (!prReviews.data.length) {
@@ -328,9 +331,10 @@ export async function getLatestReviewForReviewers(
 
 async function getGitifySubjectForRelease(
   notification: Notification,
-  token: string,
 ): Promise<GitifySubject> {
-  const release = (await getRelease(notification.subject.url, token)).data;
+  const release = (
+    await getRelease(notification.subject.url, notification.account.token)
+  ).data;
 
   return {
     state: null,


Test patch:
diff --git a/src/context/App.test.tsx b/src/context/App.test.tsx
index 25a6201d..71dc830d 100644
--- a/src/context/App.test.tsx
+++ b/src/context/App.test.tsx
@@ -1,6 +1,5 @@
 import { act, fireEvent, render, waitFor } from '@testing-library/react';
 import { useContext } from 'react';
-
 import { mockAuth, mockSettings } from '../__mocks__/state-mocks';
 import { useNotifications } from '../hooks/useNotifications';
 import type { AuthState, SettingsState } from '../types';
@@ -133,6 +132,7 @@ describe('context/App.tsx', () => {
       expect(markNotificationReadMock).toHaveBeenCalledTimes(1);
       expect(markNotificationReadMock).toHaveBeenCalledWith(
         {
+          accounts: [],
           enterpriseAccounts: [],
           token: null,
           user: null,
@@ -165,7 +165,7 @@ describe('context/App.tsx', () => {
 
       expect(markNotificationDoneMock).toHaveBeenCalledTimes(1);
       expect(markNotificationDoneMock).toHaveBeenCalledWith(
-        { enterpriseAccounts: [], token: null, user: null },
+        { accounts: [], enterpriseAccounts: [], token: null, user: null },
         mockSettings,
         '123-456',
         'github.com',
@@ -194,7 +194,7 @@ describe('context/App.tsx', () => {
 
       expect(unsubscribeNotificationMock).toHaveBeenCalledTimes(1);
       expect(unsubscribeNotificationMock).toHaveBeenCalledWith(
-        { enterpriseAccounts: [], token: null, user: null },
+        { accounts: [], enterpriseAccounts: [], token: null, user: null },
         mockSettings,
         '123-456',
         'github.com',
@@ -228,7 +228,7 @@ describe('context/App.tsx', () => {
 
       expect(markRepoNotificationsMock).toHaveBeenCalledTimes(1);
       expect(markRepoNotificationsMock).toHaveBeenCalledWith(
-        { enterpriseAccounts: [], token: null, user: null },
+        { accounts: [], enterpriseAccounts: [], token: null, user: null },
         mockSettings,
         'gitify-app/notifications-test',
         'github.com',
@@ -262,13 +262,14 @@ describe('context/App.tsx', () => {
 
       expect(markRepoNotificationsDoneMock).toHaveBeenCalledTimes(1);
       expect(markRepoNotificationsDoneMock).toHaveBeenCalledWith(
-        { enterpriseAccounts: [], token: null, user: null },
+        { accounts: [], enterpriseAccounts: [], token: null, user: null },
         mockSettings,
         'gitify-app/notifications-test',
         'github.com',
       );
     });
   });
+
   describe('authentication methods', () => {
     const apiRequestAuthMock = jest.spyOn(apiRequests, 'apiRequestAuth');
     const fetchNotificationsMock = jest.fn();
@@ -370,7 +371,7 @@ describe('context/App.tsx', () => {
       });
 
       expect(saveStateMock).toHaveBeenCalledWith({
-        auth: { enterpriseAccounts: [], token: null, user: null },
+        auth: { accounts: [], enterpriseAccounts: [], token: null, user: null },
         settings: {
           participating: true,
           playSound: true,
@@ -415,7 +416,7 @@ describe('context/App.tsx', () => {
       expect(setAutoLaunchMock).toHaveBeenCalledWith(true);
 
       expect(saveStateMock).toHaveBeenCalledWith({
-        auth: { enterpriseAccounts: [], token: null, user: null },
+        auth: { accounts: [], enterpriseAccounts: [], token: null, user: null },
         settings: {
           participating: false,
           playSound: true,
diff --git a/src/hooks/useNotifications.test.ts b/src/hooks/useNotifications.test.ts
index 83f332d2..7d55d332 100644
--- a/src/hooks/useNotifications.test.ts
+++ b/src/hooks/useNotifications.test.ts
@@ -2,8 +2,11 @@ import { act, renderHook, waitFor } from '@testing-library/react';
 import axios, { AxiosError } from 'axios';
 import nock from 'nock';
 
-import { mockAuth, mockSettings, mockUser } from '../__mocks__/state-mocks';
-import type { AuthState } from '../types';
+import {
+  mockAuth,
+  mockGitHubCloudAccount,
+  mockSettings,
+} from '../__mocks__/state-mocks';
 import { mockNotificationUser } from '../utils/api/__mocks__/response-mocks';
 import { Errors } from '../utils/constants';
 import { useNotifications } from './useNotifications';
@@ -46,10 +49,12 @@ describe('hooks/useNotifications.ts', () => {
           expect(result.current.status).toBe('success');
         });
 
-        expect(result.current.notifications[0].hostname).toBe('api.github.com');
+        expect(result.current.notifications[0].account.hostname).toBe(
+          'github.com',
+        );
         expect(result.current.notifications[0].notifications.length).toBe(2);
 
-        expect(result.current.notifications[1].hostname).toBe(
+        expect(result.current.notifications[1].account.hostname).toBe(
           'github.gitify.io',
         );
         expect(result.current.notifications[1].notifications.length).toBe(2);
@@ -100,144 +105,8 @@ describe('hooks/useNotifications.ts', () => {
       });
     });
 
-    describe('enterprise', () => {
-      it('should fetch notifications with success - enterprise only', async () => {
-        const auth: AuthState = {
-          ...mockAuth,
-          token: null,
-        };
-
-        const notifications = [
-          { id: 1, title: 'This is a notification.' },
-          { id: 2, title: 'This is another one.' },
-        ];
-
-        nock('https://github.gitify.io/api/v3/')
-          .get('/notifications?participating=false')
-          .reply(200, notifications);
-
-        const { result } = renderHook(() => useNotifications());
-
-        act(() => {
-          result.current.fetchNotifications(auth, {
-            ...mockSettings,
-            detailedNotifications: false,
-          });
-        });
-
-        await waitFor(() => {
-          expect(result.current.status).toBe('success');
-        });
-
-        expect(result.current.notifications[0].hostname).toBe(
-          'github.gitify.io',
-        );
-        expect(result.current.notifications[0].notifications.length).toBe(2);
-      });
-
-      it('should fetch notifications with failure - enterprise only', async () => {
-        const auth: AuthState = {
-          ...mockAuth,
-          token: null,
-        };
-
-        nock('https://github.gitify.io/api/v3/')
-          .get('/notifications?participating=false')
-          .replyWithError({
-            code: AxiosError.ERR_BAD_REQUEST,
-            response: {
-              status: 400,
-              data: {
-                message: 'Oops! Something went wrong.',
-              },
-            },
-          });
-
-        const { result } = renderHook(() => useNotifications());
-
-        act(() => {
-          result.current.fetchNotifications(auth, mockSettings);
-        });
-
-        await waitFor(() => {
-          expect(result.current.status).toBe('error');
-        });
-      });
-    });
-
-    describe('github.com', () => {
-      it('should fetch notifications with success - github.com only', async () => {
-        const auth: AuthState = {
-          ...mockAuth,
-          enterpriseAccounts: [],
-          user: mockUser,
-        };
-
-        const notifications = [
-          { id: 1, title: 'This is a notification.' },
-          { id: 2, title: 'This is another one.' },
-        ];
-
-        nock('https://api.github.com')
-          .get('/notifications?participating=false')
-          .reply(200, notifications);
-
-        const { result } = renderHook(() => useNotifications());
-
-        act(() => {
-          result.current.fetchNotifications(auth, {
-            ...mockSettings,
-            detailedNotifications: false,
-          });
-        });
-
-        await waitFor(() => {
-          expect(result.current.status).toBe('success');
-        });
-
-        expect(result.current.notifications[0].hostname).toBe('api.github.com');
-        expect(result.current.notifications[0].notifications.length).toBe(2);
-      });
-
-      it('should fetch notifications with failures - github.com only', async () => {
-        const auth: AuthState = {
-          ...mockAuth,
-          enterpriseAccounts: [],
-        };
-
-        nock('https://api.github.com/')
-          .get('/notifications?participating=false')
-          .replyWithError({
-            code: AxiosError.ERR_BAD_REQUEST,
-            response: {
-              status: 400,
-              data: {
-                message: 'Oops! Something went wrong.',
-              },
-            },
-          });
-
-        const { result } = renderHook(() => useNotifications());
-
-        act(() => {
-          result.current.fetchNotifications(auth, mockSettings);
-        });
-
-        await waitFor(() => {
-          expect(result.current.status).toBe('error');
-          expect(result.current.errorDetails).toBe(Errors.UNKNOWN);
-        });
-      });
-    });
-
     describe('with detailed notifications', () => {
       it('should fetch notifications with success', async () => {
-        const auth: AuthState = {
-          ...mockAuth,
-          enterpriseAccounts: [],
-          user: mockUser,
-        };
-
         const notifications = [
           {
             id: 1,
@@ -393,10 +262,15 @@ describe('hooks/useNotifications.ts', () => {
         const { result } = renderHook(() => useNotifications());
 
         act(() => {
-          result.current.fetchNotifications(auth, {
-            ...mockSettings,
-            detailedNotifications: true,
-          });
+          result.current.fetchNotifications(
+            {
+              accounts: [mockGitHubCloudAccount],
+            },
+            {
+              ...mockSettings,
+              detailedNotifications: true,
+            },
+          );
         });
 
         expect(result.current.status).toBe('loading');
@@ -405,7 +279,9 @@ describe('hooks/useNotifications.ts', () => {
           expect(result.current.status).toBe('success');
         });
 
-        expect(result.current.notifications[0].hostname).toBe('api.github.com');
+        expect(result.current.notifications[0].account.hostname).toBe(
+          'github.com',
+        );
         expect(result.current.notifications[0].notifications.length).toBe(6);
       });
     });
@@ -443,7 +319,7 @@ describe('hooks/useNotifications.ts', () => {
         result.current.removeNotificationFromState(
           mockSettings,
           result.current.notifications[0].notifications[0].id,
-          result.current.notifications[0].hostname,
+          result.current.notifications[0].account.hostname,
         );
       });
 
diff --git a/src/routes/LoginWithOAuthApp.test.tsx b/src/routes/LoginWithOAuthApp.test.tsx
index fe7a4b24..cd062d6a 100644
--- a/src/routes/LoginWithOAuthApp.test.tsx
+++ b/src/routes/LoginWithOAuthApp.test.tsx
@@ -1,7 +1,6 @@
 import { fireEvent, render, screen } from '@testing-library/react';
-import { ipcRenderer, shell } from 'electron';
+import { shell } from 'electron';
 import { MemoryRouter } from 'react-router-dom';
-import { mockEnterpriseAccounts } from '../__mocks__/state-mocks';
 import { AppContext } from '../context/App';
 import type { AuthState } from '../types';
 import { LoginWithOAuthApp, validate } from './LoginWithOAuthApp';
@@ -16,15 +15,12 @@ describe('routes/LoginWithOAuthApp.tsx', () => {
   const openExternalMock = jest.spyOn(shell, 'openExternal');
 
   const mockAuth: AuthState = {
-    enterpriseAccounts: [],
-    user: null,
+    accounts: [],
   };
 
   beforeEach(() => {
     openExternalMock.mockReset();
     mockNavigate.mockReset();
-
-    jest.spyOn(ipcRenderer, 'send');
   });
 
   it('renders correctly', () => {
@@ -111,35 +107,6 @@ describe('routes/LoginWithOAuthApp.tsx', () => {
     });
   });
 
-  it('should receive a logged-in enterprise account', () => {
-    const { rerender } = render(
-      <AppContext.Provider value={{ auth: mockAuth }}>
-        <MemoryRouter>
-          <LoginWithOAuthApp />
-        </MemoryRouter>
-      </AppContext.Provider>,
-    );
-
-    rerender(
-      <AppContext.Provider
-        value={{
-          auth: {
-            enterpriseAccounts: mockEnterpriseAccounts,
-            user: null,
-          },
-        }}
-      >
-        <MemoryRouter>
-          <LoginWithOAuthApp />
-        </MemoryRouter>
-      </AppContext.Provider>,
-    );
-
-    expect(ipcRenderer.send).toHaveBeenCalledTimes(1);
-    expect(ipcRenderer.send).toHaveBeenCalledWith('reopen-window');
-    expect(mockNavigate).toHaveBeenNthCalledWith(1, -1);
-  });
-
   it('should render the form with errors', () => {
     render(
       <AppContext.Provider value={{ auth: mockAuth }}>
diff --git a/src/routes/Settings.test.tsx b/src/routes/Settings.test.tsx
index 768443c6..2edc874b 100644
--- a/src/routes/Settings.test.tsx
+++ b/src/routes/Settings.test.tsx
@@ -1,7 +1,12 @@
 import { act, fireEvent, render, screen } from '@testing-library/react';
 import { ipcRenderer, shell } from 'electron';
 import { MemoryRouter } from 'react-router-dom';
-import { mockAuth, mockSettings } from '../__mocks__/state-mocks';
+import {
+  mockAuth,
+  mockOAuthAccount,
+  mockPersonalAccessTokenAccount,
+  mockSettings,
+} from '../__mocks__/state-mocks';
 import { mockPlatform } from '../__mocks__/utils';
 import { AppContext } from '../context/App';
 import { SettingsRoute } from './Settings';
@@ -38,7 +43,7 @@ describe('routes/Settings.tsx', () => {
       await act(async () => {
         render(
           <AppContext.Provider
-            value={{ settings: mockSettings, auth: mockAuth }}
+            value={{ auth: mockAuth, settings: mockSettings }}
           >
             <MemoryRouter>
               <SettingsRoute />
@@ -55,8 +60,8 @@ describe('routes/Settings.tsx', () => {
         render(
           <AppContext.Provider
             value={{
-              settings: mockSettings,
               auth: mockAuth,
+              settings: mockSettings,
             }}
           >
             <MemoryRouter>
@@ -76,8 +81,8 @@ describe('routes/Settings.tsx', () => {
         render(
           <AppContext.Provider
             value={{
-              settings: mockSettings,
               auth: mockAuth,
+              settings: mockSettings,
               updateSetting,
             }}
           >
@@ -99,8 +104,8 @@ describe('routes/Settings.tsx', () => {
         render(
           <AppContext.Provider
             value={{
-              settings: mockSettings,
               auth: mockAuth,
+              settings: mockSettings,
               updateSetting,
             }}
           >
@@ -127,8 +132,8 @@ describe('routes/Settings.tsx', () => {
         render(
           <AppContext.Provider
             value={{
-              settings: mockSettings,
               auth: mockAuth,
+              settings: mockSettings,
               updateSetting,
             }}
           >
@@ -154,8 +159,8 @@ describe('routes/Settings.tsx', () => {
         render(
           <AppContext.Provider
             value={{
-              settings: mockSettings,
               auth: mockAuth,
+              settings: mockSettings,
               updateSetting,
             }}
           >
@@ -179,8 +184,8 @@ describe('routes/Settings.tsx', () => {
         render(
           <AppContext.Provider
             value={{
-              settings: mockSettings,
               auth: mockAuth,
+              settings: mockSettings,
               updateSetting,
             }}
           >
@@ -214,12 +219,12 @@ describe('routes/Settings.tsx', () => {
         render(
           <AppContext.Provider
             value={{
+              auth: mockAuth,
               settings: {
                 ...mockSettings,
                 detailedNotifications: false,
                 showBots: true,
               },
-              auth: mockAuth,
               updateSetting,
             }}
           >
@@ -255,12 +260,12 @@ describe('routes/Settings.tsx', () => {
         render(
           <AppContext.Provider
             value={{
+              auth: mockAuth,
               settings: {
                 ...mockSettings,
                 detailedNotifications: true,
                 showBots: true,
               },
-              auth: mockAuth,
               updateSetting,
             }}
           >
@@ -296,8 +301,8 @@ describe('routes/Settings.tsx', () => {
         render(
           <AppContext.Provider
             value={{
-              settings: mockSettings,
               auth: mockAuth,
+              settings: mockSettings,
               updateSetting,
             }}
           >
@@ -321,8 +326,8 @@ describe('routes/Settings.tsx', () => {
         render(
           <AppContext.Provider
             value={{
-              settings: mockSettings,
               auth: mockAuth,
+              settings: mockSettings,
               updateSetting,
             }}
           >
@@ -351,8 +356,8 @@ describe('routes/Settings.tsx', () => {
         render(
           <AppContext.Provider
             value={{
-              settings: mockSettings,
               auth: mockAuth,
+              settings: mockSettings,
               updateSetting,
             }}
           >
@@ -382,8 +387,8 @@ describe('routes/Settings.tsx', () => {
         render(
           <AppContext.Provider
             value={{
-              settings: mockSettings,
               auth: mockAuth,
+              settings: mockSettings,
               updateSetting,
             }}
           >
@@ -407,8 +412,8 @@ describe('routes/Settings.tsx', () => {
         render(
           <AppContext.Provider
             value={{
-              settings: mockSettings,
               auth: mockAuth,
+              settings: mockSettings,
               updateSetting,
             }}
           >
@@ -432,8 +437,8 @@ describe('routes/Settings.tsx', () => {
         render(
           <AppContext.Provider
             value={{
-              settings: mockSettings,
               auth: mockAuth,
+              settings: mockSettings,
               updateSetting,
             }}
           >
@@ -459,8 +464,8 @@ describe('routes/Settings.tsx', () => {
         render(
           <AppContext.Provider
             value={{
-              settings: mockSettings,
               auth: mockAuth,
+              settings: mockSettings,
             }}
           >
             <MemoryRouter>
@@ -484,8 +489,8 @@ describe('routes/Settings.tsx', () => {
           render(
             <AppContext.Provider
               value={{
+                auth: { accounts: [mockOAuthAccount] },
                 settings: mockSettings,
-                auth: { ...mockAuth, token: null },
               }}
             >
               <MemoryRouter>
@@ -514,8 +519,8 @@ describe('routes/Settings.tsx', () => {
           render(
             <AppContext.Provider
               value={{
+                auth: { accounts: [mockPersonalAccessTokenAccount] },
                 settings: mockSettings,
-                auth: { ...mockAuth, token: '1234' },
               }}
             >
               <MemoryRouter>
@@ -537,11 +542,8 @@ describe('routes/Settings.tsx', () => {
           render(
             <AppContext.Provider
               value={{
+                auth: { accounts: [mockPersonalAccessTokenAccount] },
                 settings: mockSettings,
-                auth: {
-                  ...mockAuth,
-                  enterpriseAccounts: [],
-                },
               }}
             >
               <MemoryRouter>
@@ -564,8 +566,8 @@ describe('routes/Settings.tsx', () => {
           render(
             <AppContext.Provider
               value={{
+                auth: { accounts: [mockOAuthAccount] },
                 settings: mockSettings,
-                auth: mockAuth,
               }}
             >
               <MemoryRouter>
@@ -585,8 +587,8 @@ describe('routes/Settings.tsx', () => {
         render(
           <AppContext.Provider
             value={{
-              settings: mockSettings,
               auth: mockAuth,
+              settings: mockSettings,
               logout: logoutMock,
             }}
           >
@@ -611,7 +613,10 @@ describe('routes/Settings.tsx', () => {
       await act(async () => {
         render(
           <AppContext.Provider
-            value={{ settings: mockSettings, auth: mockAuth }}
+            value={{
+              auth: mockAuth,
+              settings: mockSettings,
+            }}
           >
             <MemoryRouter>
               <SettingsRoute />
diff --git a/src/routes/__snapshots__/Settings.test.tsx.snap b/src/routes/__snapshots__/Settings.test.tsx.snap
index c0eb68f2..e95556fe 100644
--- a/src/routes/__snapshots__/Settings.test.tsx.snap
+++ b/src/routes/__snapshots__/Settings.test.tsx.snap
@@ -549,7 +549,6 @@ exports[`routes/Settings.tsx General should render itself & its children 1`] = `
       </button>
       <button
         class="hover:text-gray-500 py-1 px-2 my-1 mx-2 focus:outline-none"
-        hidden=""
         title="Login with OAuth App"
         type="button"
       >
diff --git a/src/utils/api/__snapshots__/client.test.ts.snap b/src/utils/api/__snapshots__/client.test.ts.snap
index f793d09c..7f0db319 100644
--- a/src/utils/api/__snapshots__/client.test.ts.snap
+++ b/src/utils/api/__snapshots__/client.test.ts.snap
@@ -3,7 +3,7 @@
 exports[`utils/api/client.ts getAuthenticatedUser should fetch authenticated user - enterprise 1`] = `
 {
   "Accept": "application/json",
-  "Authorization": "token yourAuthToken",
+  "Authorization": "token token-123-456",
   "Cache-Control": "no-cache",
   "Content-Type": "application/json",
 }
@@ -12,7 +12,7 @@ exports[`utils/api/client.ts getAuthenticatedUser should fetch authenticated use
 exports[`utils/api/client.ts getAuthenticatedUser should fetch authenticated user - github 1`] = `
 {
   "Accept": "application/json",
-  "Authorization": "token yourAuthToken",
+  "Authorization": "token token-123-456",
   "Cache-Control": "no-cache",
   "Content-Type": "application/json",
 }
@@ -21,7 +21,7 @@ exports[`utils/api/client.ts getAuthenticatedUser should fetch authenticated use
 exports[`utils/api/client.ts headNotifications should fetch notifications head - enterprise 1`] = `
 {
   "Accept": "application/json",
-  "Authorization": "token yourAuthToken",
+  "Authorization": "token token-123-456",
   "Cache-Control": "no-cache",
   "Content-Type": "application/json",
 }
@@ -30,7 +30,7 @@ exports[`utils/api/client.ts headNotifications should fetch notifications head -
 exports[`utils/api/client.ts headNotifications should fetch notifications head - github 1`] = `
 {
   "Accept": "application/json",
-  "Authorization": "token yourAuthToken",
+  "Authorization": "token token-123-456",
   "Cache-Control": "no-cache",
   "Content-Type": "application/json",
 }
@@ -39,7 +39,7 @@ exports[`utils/api/client.ts headNotifications should fetch notifications head -
 exports[`utils/api/client.ts ignoreNotificationThreadSubscription should ignore notification thread subscription - github 1`] = `
 {
   "Accept": "application/json",
-  "Authorization": "token yourAuthToken",
+  "Authorization": "token token-123-456",
   "Cache-Control": "no-cache",
   "Content-Type": "application/json",
 }
@@ -48,25 +48,25 @@ exports[`utils/api/client.ts ignoreNotificationThreadSubscription should ignore
 exports[`utils/api/client.ts ignoreNotificationThreadSubscription should ignore notification thread subscription- enterprise 1`] = `
 {
   "Accept": "application/json",
-  "Authorization": "token yourAuthToken",
+  "Authorization": "token token-123-456",
   "Cache-Control": "no-cache",
   "Content-Type": "application/json",
 }
 `;
 
-exports[`utils/api/client.ts listNotificationsForAuthenticatedUser should list notifications for user - enterprise 1`] = `
+exports[`utils/api/client.ts listNotificationsForAuthenticatedUser should list notifications for user - github cloud 1`] = `
 {
   "Accept": "application/json",
-  "Authorization": "token yourAuthToken",
+  "Authorization": "token token-123-456",
   "Cache-Control": "no-cache",
   "Content-Type": "application/json",
 }
 `;
 
-exports[`utils/api/client.ts listNotificationsForAuthenticatedUser should list notifications for user - github 1`] = `
+exports[`utils/api/client.ts listNotificationsForAuthenticatedUser should list notifications for user - github enterprise server 1`] = `
 {
   "Accept": "application/json",
-  "Authorization": "token yourAuthToken",
+  "Authorization": "token 1234568790",
   "Cache-Control": "no-cache",
   "Content-Type": "application/json",
 }
@@ -75,7 +75,7 @@ exports[`utils/api/client.ts listNotificationsForAuthenticatedUser should list n
 exports[`utils/api/client.ts markNotificationThreadAsDone should mark notification thread as done - github 1`] = `
 {
   "Accept": "application/json",
-  "Authorization": "token yourAuthToken",
+  "Authorization": "token token-123-456",
   "Cache-Control": "no-cache",
   "Content-Type": "application/json",
 }
@@ -84,7 +84,7 @@ exports[`utils/api/client.ts markNotificationThreadAsDone should mark notificati
 exports[`utils/api/client.ts markNotificationThreadAsDone should mark notification thread as done- enterprise 1`] = `
 {
   "Accept": "application/json",
-  "Authorization": "token yourAuthToken",
+  "Authorization": "token token-123-456",
   "Cache-Control": "no-cache",
   "Content-Type": "application/json",
 }
@@ -93,7 +93,7 @@ exports[`utils/api/client.ts markNotificationThreadAsDone should mark notificati
 exports[`utils/api/client.ts markNotificationThreadAsRead should mark notification thread as read - github 1`] = `
 {
   "Accept": "application/json",
-  "Authorization": "token yourAuthToken",
+  "Authorization": "token token-123-456",
   "Cache-Control": "no-cache",
   "Content-Type": "application/json",
 }
@@ -102,7 +102,7 @@ exports[`utils/api/client.ts markNotificationThreadAsRead should mark notificati
 exports[`utils/api/client.ts markNotificationThreadAsRead should mark notification thread as read- enterprise 1`] = `
 {
   "Accept": "application/json",
-  "Authorization": "token yourAuthToken",
+  "Authorization": "token token-123-456",
   "Cache-Control": "no-cache",
   "Content-Type": "application/json",
 }
@@ -111,7 +111,7 @@ exports[`utils/api/client.ts markNotificationThreadAsRead should mark notificati
 exports[`utils/api/client.ts markRepositoryNotificationsAsRead should mark repository notifications as read - enterprise 1`] = `
 {
   "Accept": "application/json",
-  "Authorization": "token yourAuthToken",
+  "Authorization": "token token-123-456",
   "Cache-Control": "no-cache",
   "Content-Type": "application/json",
 }
@@ -120,7 +120,7 @@ exports[`utils/api/client.ts markRepositoryNotificationsAsRead should mark repos
 exports[`utils/api/client.ts markRepositoryNotificationsAsRead should mark repository notifications as read - github 1`] = `
 {
   "Accept": "application/json",
-  "Authorization": "token yourAuthToken",
+  "Authorization": "token token-123-456",
   "Cache-Control": "no-cache",
   "Content-Type": "application/json",
 }
diff --git a/src/utils/api/client.test.ts b/src/utils/api/client.test.ts
index ee7b28e4..526866f8 100644
--- a/src/utils/api/client.test.ts
+++ b/src/utils/api/client.test.ts
@@ -1,4 +1,9 @@
 import axios, { type AxiosPromise, type AxiosResponse } from 'axios';
+import {
+  mockGitHubCloudAccount,
+  mockGitHubEnterpriseServerAccount,
+  mockToken,
+} from '../../__mocks__/state-mocks';
 import type { SettingsState } from '../../types';
 import {
   getAuthenticatedUser,
@@ -16,7 +21,6 @@ jest.mock('axios');
 
 const mockGitHubHostname = 'github.com';
 const mockEnterpriseHostname = 'example.com';
-const mockToken = 'yourAuthToken';
 const mockThreadId = '1234';
 const mockRepoSlug = 'gitify-app/notifications-test';
 
@@ -82,10 +86,9 @@ describe('utils/api/client.ts', () => {
       participating: true,
     };
 
-    it('should list notifications for user - github', async () => {
+    it('should list notifications for user - github cloud', async () => {
       await listNotificationsForAuthenticatedUser(
-        mockGitHubHostname,
-        mockToken,
+        mockGitHubCloudAccount,
         mockSettings as SettingsState,
       );
 
@@ -98,15 +101,14 @@ describe('utils/api/client.ts', () => {
       expect(axios.defaults.headers.common).toMatchSnapshot();
     });
 
-    it('should list notifications for user - enterprise', async () => {
+    it('should list notifications for user - github enterprise server', async () => {
       await listNotificationsForAuthenticatedUser(
-        mockEnterpriseHostname,
-        mockToken,
+        mockGitHubEnterpriseServerAccount,
         mockSettings as SettingsState,
       );
 
       expect(axios).toHaveBeenCalledWith({
-        url: 'https://example.com/api/v3/notifications?participating=true',
+        url: 'https://github.gitify.io/api/v3/notifications?participating=true',
         method: 'GET',
         data: {},
       });
diff --git a/src/utils/auth/auth.test.ts b/src/utils/auth/auth.test.ts
deleted file mode 100644
index 2a5810ec..00000000
--- a/src/utils/auth/auth.test.ts
+++ /dev/null
@@ -1,213 +0,0 @@
-import remote from '@electron/remote';
-import type { AxiosPromise, AxiosResponse } from 'axios';
-import type { AuthState } from '../../types';
-import * as apiRequests from '../api/request';
-import * as auth from './utils';
-import { getNewOAuthAppURL, getNewTokenURL } from './utils';
-
-const browserWindow = new remote.BrowserWindow();
-
-describe('utils/auth.tsx', () => {
-  describe('authGitHub', () => {
-    const loadURLMock = jest.spyOn(browserWindow, 'loadURL');
-
-    beforeEach(() => {
-      loadURLMock.mockReset();
-    });
-
-    it('should call authGitHub - success', async () => {
-      // Casting to jest.Mock avoids Typescript errors, where the spy is expected to match all the original
-      // function's typing. I might fix all that if the return type of this was actually used, or if I was
-      // writing this test for a new feature. Since I'm just upgrading Jest, jest.Mock is a nice escape hatch
-      (
-        jest.spyOn(browserWindow.webContents, 'on') as jest.Mock
-      ).mockImplementation((event, callback): void => {
-        if (event === 'will-redirect') {
-          const event = new Event('will-redirect');
-          callback(event, 'http://github.com/?code=123-456');
-        }
-      });
-
-      const res = await auth.authGitHub();
-
-      expect(res.authCode).toBe('123-456');
-
-      expect(
-        browserWindow.webContents.session.clearStorageData,
-      ).toHaveBeenCalledTimes(1);
-
-      expect(loadURLMock).toHaveBeenCalledTimes(1);
-      expect(loadURLMock).toHaveBeenCalledWith(
-        'https://github.com/login/oauth/authorize?client_id=FAKE_CLIENT_ID_123&scope=read:user,notifications,repo',
-      );
-
-      expect(browserWindow.destroy).toHaveBeenCalledTimes(1);
-    });
-
-    it('should call authGitHub - failure', async () => {
-      (
-        jest.spyOn(browserWindow.webContents, 'on') as jest.Mock
-      ).mockImplementation((event, callback): void => {
-        if (event === 'will-redirect') {
-          const event = new Event('will-redirect');
-          callback(event, 'http://www.github.com/?error=Oops');
-        }
-      });
-
-      await expect(async () => await auth.authGitHub()).rejects.toEqual(
-        "Oops! Something went wrong and we couldn't log you in using GitHub. Please try again.",
-      );
-      expect(loadURLMock).toHaveBeenCalledTimes(1);
-    });
-  });
-
-  describe('getToken', () => {
-    const authCode = '123-456';
-    const apiRequestMock = jest.spyOn(apiRequests, 'apiRequest');
-
-    it('should get a token - success', async () => {
-      const requestPromise = new Promise((resolve) =>
-        resolve({ data: { access_token: 'this-is-a-token' } } as AxiosResponse),
-      ) as AxiosPromise;
-
-      apiRequestMock.mockResolvedValueOnce(requestPromise);
-
-      const res = await auth.getToken(authCode);
-
-      expect(apiRequests.apiRequest).toHaveBeenCalledWith(
-        'https://github.com/login/oauth/access_token',
-        'POST',
-        {
-          client_id: 'FAKE_CLIENT_ID_123',
-          client_secret: 'FAKE_CLIENT_SECRET_123',
-          code: '123-456',
-        },
-      );
-      expect(res.token).toBe('this-is-a-token');
-      expect(res.hostname).toBe('github.com');
-    });
-
-    it('should get a token - failure', async () => {
-      const message = 'Something went wrong.';
-
-      const requestPromise = new Promise((_, reject) =>
-        reject({ data: { message } } as AxiosResponse),
-      ) as AxiosPromise;
-
-      apiRequestMock.mockResolvedValueOnce(requestPromise);
-
-      const call = async () => await auth.getToken(authCode);
-
-      await expect(call).rejects.toEqual({ data: { message } });
-    });
-  });
-
-  describe('addAccount', () => {
-    const mockAuth: AuthState = {
-      token: null,
-      enterpriseAccounts: [],
-      user: null,
-    };
-
-    it('should add a github.com account', () => {
-      const result = auth.addAccount(mockAuth, '123-456', 'github.com');
-
-      expect(result).toEqual({ ...mockAuth, token: '123-456' });
-    });
-
-    it('should add an enterprise account', () => {
-      const result = auth.addAccount(mockAuth, '123-456', 'github.gitify.io');
-
-      expect(result).toEqual({
-        ...mockAuth,
-        enterpriseAccounts: [
-          { hostname: 'github.gitify.io', token: '123-456' },
-        ],
-      });
-    });
-  });
-
-  describe('getNewTokenURL', () => {
-    it('should generate new PAT url - github cloud', () => {
-      expect(
-        getNewTokenURL('github.com').startsWith(
-          'https://github.com/settings/tokens/new',
-        ),
-      ).toBeTruthy();
-    });
-
-    it('should generate new PAT url - github server', () => {
-      expect(
-        getNewTokenURL('github.gitify.io').startsWith(
-          'https://github.gitify.io/settings/tokens/new',
-        ),
-      ).toBeTruthy();
-    });
-  });
-
-  describe('getNewOAuthAppURL', () => {
-    it('should generate new oauth app url - github cloud', () => {
-      expect(
-        getNewOAuthAppURL('github.com').startsWith(
-          'https://github.com/settings/applications/new',
-        ),
-      ).toBeTruthy();
-    });
-
-    it('should generate new oauth app url - github server', () => {
-      expect(
-        getNewOAuthAppURL('github.gitify.io').startsWith(
-          'https://github.gitify.io/settings/applications/new',
-        ),
-      ).toBeTruthy();
-    });
-  });
-
-  describe('isValidHostname', () => {
-    it('should validate hostname - github cloud', () => {
-      expect(auth.isValidHostname('github.com')).toBeTruthy();
-    });
-
-    it('should validate hostname - github enterprise server', () => {
-      expect(auth.isValidHostname('github.gitify.io')).toBeTruthy();
-    });
-
-    it('should invalidate hostname - empty', () => {
-      expect(auth.isValidHostname('')).toBeFalsy();
-    });
-
-    it('should invalidate hostname - invalid', () => {
-      expect(auth.isValidHostname('github')).toBeFalsy();
-    });
-  });
-
-  describe('isValidClientId', () => {
-    it('should validate client id - valid', () => {
-      expect(auth.isValidClientId('1234567890_ASDFGHJKL')).toBeTruthy();
-    });
-
-    it('should validate client id - empty', () => {
-      expect(auth.isValidClientId('')).toBeFalsy();
-    });
-
-    it('should validate client id - invalid', () => {
-      expect(auth.isValidClientId('1234567890asdfg')).toBeFalsy();
-    });
-  });
-
-  describe('isValidToken', () => {
-    it('should validate token - valid', () => {
-      expect(
-        auth.isValidToken('1234567890_asdfghjklPOIUYTREWQ0987654321'),
-      ).toBeTruthy();
-    });
-
-    it('should validate token - empty', () => {
-      expect(auth.isValidToken('')).toBeFalsy();
-    });
-
-    it('should validate token - invalid', () => {
-      expect(auth.isValidToken('1234567890asdfg')).toBeFalsy();
-    });
-  });
-});
diff --git a/src/utils/auth/migration.test.ts b/src/utils/auth/migration.test.ts
new file mode 100644
index 00000000..9a65101b
--- /dev/null
+++ b/src/utils/auth/migration.test.ts
@@ -0,0 +1,154 @@
+import axios from 'axios';
+import nock from 'nock';
+import { mockGitifyUser, mockToken } from '../../__mocks__/state-mocks';
+import type { AuthState } from '../../types';
+import Constants from '../constants';
+import {
+  convertAccounts,
+  hasAccountsToMigrate,
+  migrateAuthenticatedAccounts,
+} from './migration';
+
+describe('utils/auth/migration.ts', () => {
+  beforeEach(() => {
+    // axios will default to using the XHR adapter which can't be intercepted
+    // by nock. So, configure axios to use the node adapter.
+    axios.defaults.adapter = 'http';
+  });
+
+  describe('migrateAuthenticatedAccounts', () => {
+    it('migrate and save legacy accounts', async () => {
+      jest.spyOn(localStorage.__proto__, 'getItem').mockReturnValueOnce(
+        JSON.stringify({
+          auth: {
+            token: mockToken,
+          },
+          settings: { theme: 'DARK' },
+        }),
+      );
+
+      nock('https://api.github.com').get('/user').reply(200, mockGitifyUser);
+
+      jest.spyOn(localStorage.__proto__, 'setItem');
+
+      await migrateAuthenticatedAccounts();
+
+      expect(localStorage.setItem).toHaveBeenCalledTimes(1);
+    });
+  });
+
+  describe('hasAccountsToMigrate', () => {
+    it('should not migrate if none exist', () => {
+      expect(hasAccountsToMigrate(null)).toBe(false);
+    });
+
+    it('should not migrate if empty', () => {
+      expect(hasAccountsToMigrate({} as AuthState)).toBe(false);
+    });
+
+    it('should migrate if token exist', () => {
+      expect(
+        hasAccountsToMigrate({
+          token: mockToken,
+        } as AuthState),
+      ).toBe(true);
+    });
+
+    it('should migrate if token exist', () => {
+      expect(
+        hasAccountsToMigrate({
+          enterpriseAccounts: [
+            { hostname: 'github.gitify.io', token: mockToken },
+          ],
+        } as AuthState),
+      ).toBe(true);
+    });
+
+    it('should not if there are already accounts', () => {
+      expect(
+        hasAccountsToMigrate({
+          token: mockToken,
+          enterpriseAccounts: [
+            { hostname: 'github.gitify.io', token: mockToken },
+          ],
+          accounts: [{ hostname: 'github.com', token: mockToken }],
+        } as AuthState),
+      ).toBe(false);
+    });
+  });
+
+  describe('convertAccounts', () => {
+    it('should convert accounts - personal access token only', async () => {
+      nock('https://api.github.com').get('/user').reply(200, mockGitifyUser);
+
+      const result = await convertAccounts({
+        token: mockToken,
+        user: mockGitifyUser,
+      } as AuthState);
+
+      expect(result).toEqual([
+        {
+          hostname: Constants.DEFAULT_AUTH_OPTIONS.hostname,
+          platform: 'GitHub Cloud',
+          method: 'Personal Access Token',
+          token: mockToken,
+          user: mockGitifyUser,
+        },
+      ]);
+    });
+
+    it('should convert accounts - oauth app only', async () => {
+      nock('https://github.gitify.io/api/v3')
+        .get('/user')
+        .reply(200, mockGitifyUser);
+
+      const result = await convertAccounts({
+        enterpriseAccounts: [
+          { hostname: 'github.gitify.io', token: mockToken },
+        ],
+      } as AuthState);
+
+      expect(result).toEqual([
+        {
+          hostname: 'github.gitify.io',
+          platform: 'GitHub Enterprise Server',
+          method: 'OAuth App',
+          token: mockToken,
+          user: mockGitifyUser,
+        },
+      ]);
+    });
+
+    it('should convert accounts - combination', async () => {
+      nock('https://api.github.com').get('/user').reply(200, mockGitifyUser);
+      nock('https://github.gitify.io/api/v3')
+        .get('/user')
+        .reply(200, mockGitifyUser);
+
+      const result = await convertAccounts({
+        token: mockToken,
+        user: mockGitifyUser,
+        enterpriseAccounts: [
+          { hostname: 'github.gitify.io', token: mockToken },
+        ],
+      } as AuthState);
+
+      expect(result).toEqual([
+        {
+          hostname: Constants.DEFAULT_AUTH_OPTIONS.hostname,
+          platform: 'GitHub Cloud',
+          method: 'Personal Access Token',
+          token: mockToken,
+          user: mockGitifyUser,
+        },
+        {
+          hostname: 'github.gitify.io',
+          platform: 'GitHub Enterprise Server',
+          method: 'OAuth App',
+          token: mockToken,
+          user: mockGitifyUser,
+        },
+      ]);
+    });
+  });
+});
diff --git a/src/utils/auth/utils.test.ts b/src/utils/auth/utils.test.ts
index 46edb356..6afe6d7e 100644
--- a/src/utils/auth/utils.test.ts
+++ b/src/utils/auth/utils.test.ts
@@ -103,26 +103,91 @@ describe('utils/auth/utils.ts', () => {
   });
 
   describe('addAccount', () => {
-    const mockAuth: AuthState = {
-      token: null,
-      enterpriseAccounts: [],
-      user: null,
-    };
+    let mockAuthState: AuthState;
 
-    it('should add a github.com account', () => {
-      const result = auth.addAccount(mockAuth, '123-456', 'github.com');
+    beforeEach(() => {
+      mockAuthState = {
+        accounts: [],
+      };
+    });
+
+    describe('should add GitHub Cloud account', () => {
+      it('should add personal access token account', async () => {
+        const result = await auth.addAccount(
+          mockAuthState,
+          'Personal Access Token',
+          '123-456',
+          'github.com',
+        );
+
+        expect(result.accounts).toEqual([
+          {
+            hostname: 'github.com',
+            method: 'Personal Access Token',
+            platform: 'GitHub Cloud',
+            token: '123-456',
+            user: undefined,
+          },
+        ]);
+      });
 
-      expect(result).toEqual({ ...mockAuth, token: '123-456' });
+      it('should add oauth app account', async () => {
+        const result = await auth.addAccount(
+          mockAuthState,
+          'OAuth App',
+          '123-456',
+          'github.com',
+        );
+
+        expect(result.accounts).toEqual([
+          {
+            hostname: 'github.com',
+            method: 'OAuth App',
+            platform: 'GitHub Cloud',
+            token: '123-456',
+            user: undefined,
+          },
+        ]);
+      });
     });
 
-    it('should add an enterprise account', () => {
-      const result = auth.addAccount(mockAuth, '123-456', 'github.gitify.io');
+    describe('should add GitHub Enterprise Server account', () => {
+      it('should add personal access token account', async () => {
+        const result = await auth.addAccount(
+          mockAuthState,
+          'Personal Access Token',
+          '123-456',
+          'github.gitify.io',
+        );
+
+        expect(result.accounts).toEqual([
+          {
+            hostname: 'github.gitify.io',
+            method: 'Personal Access Token',
+            platform: 'GitHub Enterprise Server',
+            token: '123-456',
+            user: undefined,
+          },
+        ]);
+      });
 
-      expect(result).toEqual({
-        ...mockAuth,
-        enterpriseAccounts: [
-          { hostname: 'github.gitify.io', token: '123-456' },
-        ],
+      it('should add oauth app account', async () => {
+        const result = await auth.addAccount(
+          mockAuthState,
+          'OAuth App',
+          '123-456',
+          'github.gitify.io',
+        );
+
+        expect(result.accounts).toEqual([
+          {
+            hostname: 'github.gitify.io',
+            method: 'OAuth App',
+            platform: 'GitHub Enterprise Server',
+            token: '123-456',
+            user: undefined,
+          },
+        ]);
       });
     });
   });
diff --git a/src/utils/helpers.test.ts b/src/utils/helpers.test.ts
index e989213a..ebabde89 100644
--- a/src/utils/helpers.test.ts
+++ b/src/utils/helpers.test.ts
@@ -1,5 +1,9 @@
 import type { AxiosPromise, AxiosResponse } from 'axios';
-import { mockAuth, mockUser } from '../__mocks__/state-mocks';
+import {
+  mockOAuthAccount,
+  mockPersonalAccessTokenAccount,
+} from '../__mocks__/state-mocks';
+
 import type { SubjectType } from '../typesGitHub';
 import {
   mockGraphQLResponse,
@@ -11,6 +15,7 @@ import {
   formatNotificationUpdatedAt,
   generateGitHubWebUrl,
   generateNotificationReferrerId,
+  getPlatformFromHostname,
   isEnterpriseHost,
   isOAuthAppLoggedIn,
   isPersonalAccessTokenLoggedIn,
@@ -20,14 +25,24 @@ describe('utils/helpers.ts', () => {
   describe('isPersonalAccessTokenLoggedIn', () => {
     it('logged in', () => {
       expect(
-        isPersonalAccessTokenLoggedIn({ ...mockAuth, token: '1234' }),
+        isPersonalAccessTokenLoggedIn({
+          accounts: [mockPersonalAccessTokenAccount],
+        }),
       ).toBe(true);
     });
 
     it('logged out', () => {
-      expect(isPersonalAccessTokenLoggedIn({ ...mockAuth, token: null })).toBe(
-        false,
-      );
+      expect(
+        isPersonalAccessTokenLoggedIn({
+          accounts: [],
+        }),
+      ).toBe(false);
+
+      expect(
+        isPersonalAccessTokenLoggedIn({
+          accounts: [mockOAuthAccount],
+        }),
+      ).toBe(false);
     });
   });
 
@@ -35,19 +50,32 @@ describe('utils/helpers.ts', () => {
     it('logged in', () => {
       expect(
         isOAuthAppLoggedIn({
-          ...mockAuth,
-          enterpriseAccounts: [{ hostname: 'github.gitify.io', token: '1234' }],
+          accounts: [mockOAuthAccount],
         }),
       ).toBe(true);
     });
 
     it('logged out', () => {
+      expect(isOAuthAppLoggedIn({ accounts: [] })).toBe(false);
+
       expect(
-        isOAuthAppLoggedIn({ ...mockAuth, enterpriseAccounts: null }),
+        isOAuthAppLoggedIn({ accounts: [mockPersonalAccessTokenAccount] }),
       ).toBe(false);
+    });
+  });
 
-      expect(isOAuthAppLoggedIn({ ...mockAuth, enterpriseAccounts: [] })).toBe(
-        false,
+  describe('getPlatformFromHostname', () => {
+    it('should return GitHub Cloud', () => {
+      expect(getPlatformFromHostname('github.com')).toBe('GitHub Cloud');
+      expect(getPlatformFromHostname('api.github.com')).toBe('GitHub Cloud');
+    });
+
+    it('should return GitHub Enterprise Server', () => {
+      expect(getPlatformFromHostname('github.gitify.app')).toBe(
+        'GitHub Enterprise Server',
+      );
+      expect(getPlatformFromHostname('api.github.gitify.app')).toBe(
+        'GitHub Enterprise Server',
       );
     });
   });
@@ -66,10 +94,7 @@ describe('utils/helpers.ts', () => {
 
   describe('generateNotificationReferrerId', () => {
     it('should generate the notification_referrer_id', () => {
-      const referrerId = generateNotificationReferrerId(
-        mockSingleNotification.id,
-        mockUser.id,
-      );
+      const referrerId = generateNotificationReferrerId(mockSingleNotification);
       expect(referrerId).toBe(
         'MDE4Ok5vdGlmaWNhdGlvblRocmVhZDEzODY2MTA5NjoxMjM0NTY3ODk=',
       );
@@ -106,19 +131,16 @@ describe('utils/helpers.ts', () => {
 
       apiRequestAuthMock.mockResolvedValue(requestPromise);
 
-      const result = await generateGitHubWebUrl(
-        {
-          ...mockSingleNotification,
-          subject: subject,
-        },
-        mockAuth,
-      );
+      const result = await generateGitHubWebUrl({
+        ...mockSingleNotification,
+        subject: subject,
+      });
 
       expect(apiRequestAuthMock).toHaveBeenCalledTimes(1);
       expect(apiRequestAuthMock).toHaveBeenCalledWith(
         subject.latest_comment_url,
         'GET',
-        mockAuth.token,
+        mockPersonalAccessTokenAccount.token,
       );
       expect(result).toBe(`${mockHtmlUrl}?${mockNotificationReferrer}`);
     });
@@ -141,19 +163,16 @@ describe('utils/helpers.ts', () => {
 
       apiRequestAuthMock.mockResolvedValue(requestPromise);
 
-      const result = await generateGitHubWebUrl(
-        {
-          ...mockSingleNotification,
-          subject: subject,
-        },
-        mockAuth,
-      );
+      const result = await generateGitHubWebUrl({
+        ...mockSingleNotification,
+        subject: subject,
+      });
 
       expect(apiRequestAuthMock).toHaveBeenCalledTimes(1);
       expect(apiRequestAuthMock).toHaveBeenCalledWith(
         subject.url,
         'GET',
-        mockAuth.token,
+        mockPersonalAccessTokenAccount.token,
       );
       expect(result).toBe(`${mockHtmlUrl}?${mockNotificationReferrer}`);
     });
@@ -167,13 +186,10 @@ describe('utils/helpers.ts', () => {
           type: 'CheckSuite' as SubjectType,
         };
 
-        const result = await generateGitHubWebUrl(
-          {
-            ...mockSingleNotification,
-            subject: subject,
-          },
-          mockAuth,
-        );
+        const result = await generateGitHubWebUrl({
+          ...mockSingleNotification,
+          subject: subject,
+        });
 
         expect(apiRequestAuthMock).toHaveBeenCalledTimes(0);
         expect(result).toBe(
@@ -189,13 +205,10 @@ describe('utils/helpers.ts', () => {
           type: 'CheckSuite' as SubjectType,
         };
 
-        const result = await generateGitHubWebUrl(
-          {
-            ...mockSingleNotification,
-            subject: subject,
-          },
-          mockAuth,
-        );
+        const result = await generateGitHubWebUrl({
+          ...mockSingleNotification,
+          subject: subject,
+        });
 
         expect(apiRequestAuthMock).toHaveBeenCalledTimes(0);
         expect(result).toBe(
@@ -211,13 +224,10 @@ describe('utils/helpers.ts', () => {
           type: 'CheckSuite' as SubjectType,
         };
 
-        const result = await generateGitHubWebUrl(
-          {
-            ...mockSingleNotification,
-            subject: subject,
-          },
-          mockAuth,
-        );
+        const result = await generateGitHubWebUrl({
+          ...mockSingleNotification,
+          subject: subject,
+        });
 
         expect(apiRequestAuthMock).toHaveBeenCalledTimes(0);
         expect(result).toBe(
@@ -233,13 +243,10 @@ describe('utils/helpers.ts', () => {
           type: 'CheckSuite' as SubjectType,
         };
 
-        const result = await generateGitHubWebUrl(
-          {
-            ...mockSingleNotification,
-            subject: subject,
-          },
-          mockAuth,
-        );
+        const result = await generateGitHubWebUrl({
+          ...mockSingleNotification,
+          subject: subject,
+        });
 
         expect(apiRequestAuthMock).toHaveBeenCalledTimes(0);
         expect(result).toBe(
@@ -255,13 +262,10 @@ describe('utils/helpers.ts', () => {
           type: 'CheckSuite' as SubjectType,
         };
 
-        const result = await generateGitHubWebUrl(
-          {
-            ...mockSingleNotification,
-            subject: subject,
-          },
-          mockAuth,
-        );
+        const result = await generateGitHubWebUrl({
+          ...mockSingleNotification,
+          subject: subject,
+        });
 
         expect(apiRequestAuthMock).toHaveBeenCalledTimes(0);
         expect(result).toBe(
@@ -277,13 +281,10 @@ describe('utils/helpers.ts', () => {
           type: 'CheckSuite' as SubjectType,
         };
 
-        const result = await generateGitHubWebUrl(
-          {
-            ...mockSingleNotification,
-            subject: subject,
-          },
-          mockAuth,
-        );
+        const result = await generateGitHubWebUrl({
+          ...mockSingleNotification,
+          subject: subject,
+        });
 
         expect(apiRequestAuthMock).toHaveBeenCalledTimes(0);
         expect(result).toBe(
@@ -299,13 +300,10 @@ describe('utils/helpers.ts', () => {
           type: 'CheckSuite' as SubjectType,
         };
 
-        const result = await generateGitHubWebUrl(
-          {
-            ...mockSingleNotification,
-            subject: subject,
-          },
-          mockAuth,
-        );
+        const result = await generateGitHubWebUrl({
+          ...mockSingleNotification,
+          subject: subject,
+        });
 
         expect(apiRequestAuthMock).toHaveBeenCalledTimes(0);
         expect(result).toBe(
@@ -331,13 +329,10 @@ describe('utils/helpers.ts', () => {
 
         apiRequestAuthMock.mockResolvedValue(requestPromise);
 
-        const result = await generateGitHubWebUrl(
-          {
-            ...mockSingleNotification,
-            subject: subject,
-          },
-          mockAuth,
-        );
+        const result = await generateGitHubWebUrl({
+          ...mockSingleNotification,
+          subject: subject,
+        });
 
         expect(apiRequestAuthMock).toHaveBeenCalledTimes(1);
         expect(result).toBe(
@@ -363,13 +358,10 @@ describe('utils/helpers.ts', () => {
 
         apiRequestAuthMock.mockResolvedValue(requestPromise);
 
-        const result = await generateGitHubWebUrl(
-          {
-            ...mockSingleNotification,
-            subject: subject,
-          },
-          mockAuth,
-        );
+        const result = await generateGitHubWebUrl({
+          ...mockSingleNotification,
+          subject: subject,
+        });
 
         expect(apiRequestAuthMock).toHaveBeenCalledTimes(1);
         expect(result).toBe(
@@ -391,13 +383,10 @@ describe('utils/helpers.ts', () => {
 
         apiRequestAuthMock.mockResolvedValue(requestPromise);
 
-        const result = await generateGitHubWebUrl(
-          {
-            ...mockSingleNotification,
-            subject: subject,
-          },
-          mockAuth,
-        );
+        const result = await generateGitHubWebUrl({
+          ...mockSingleNotification,
+          subject: subject,
+        });
 
         expect(apiRequestAuthMock).toHaveBeenCalledTimes(1);
         expect(result).toBe(
@@ -415,13 +404,10 @@ describe('utils/helpers.ts', () => {
         type: 'RepositoryInvitation' as SubjectType,
       };
 
-      const result = await generateGitHubWebUrl(
-        {
-          ...mockSingleNotification,
-          subject: subject,
-        },
-        mockAuth,
-      );
+      const result = await generateGitHubWebUrl({
+        ...mockSingleNotification,
+        subject: subject,
+      });
 
       expect(apiRequestAuthMock).toHaveBeenCalledTimes(0);
       expect(result).toBe(
@@ -438,13 +424,10 @@ describe('utils/helpers.ts', () => {
           type: 'WorkflowRun' as SubjectType,
         };
 
-        const result = await generateGitHubWebUrl(
-          {
-            ...mockSingleNotification,
-            subject: subject,
-          },
-          mockAuth,
-        );
+        const result = await generateGitHubWebUrl({
+          ...mockSingleNotification,
+          subject: subject,
+        });
 
         expect(apiRequestAuthMock).toHaveBeenCalledTimes(0);
         expect(result).toBe(
@@ -461,13 +444,10 @@ describe('utils/helpers.ts', () => {
           type: 'WorkflowRun' as SubjectType,
         };
 
-        const result = await generateGitHubWebUrl(
-          {
-            ...mockSingleNotification,
-            subject: subject,
-          },
-          mockAuth,
-        );
+        const result = await generateGitHubWebUrl({
+          ...mockSingleNotification,
+          subject: subject,
+        });
 
         expect(apiRequestAuthMock).toHaveBeenCalledTimes(0);
         expect(result).toBe(
@@ -483,13 +463,10 @@ describe('utils/helpers.ts', () => {
           type: 'WorkflowRun' as SubjectType,
         };
 
-        const result = await generateGitHubWebUrl(
-          {
-            ...mockSingleNotification,
-            subject: subject,
-          },
-          mockAuth,
-        );
+        const result = await generateGitHubWebUrl({
+          ...mockSingleNotification,
+          subject: subject,
+        });
 
         expect(apiRequestAuthMock).toHaveBeenCalledTimes(0);
         expect(result).toBe(
@@ -506,13 +483,10 @@ describe('utils/helpers.ts', () => {
         type: 'Issue' as SubjectType,
       };
 
-      const result = await generateGitHubWebUrl(
-        {
-          ...mockSingleNotification,
-          subject: subject,
-        },
-        mockAuth,
-      );
+      const result = await generateGitHubWebUrl({
+        ...mockSingleNotification,
+        subject: subject,
+      });
 
       expect(apiRequestAuthMock).toHaveBeenCalledTimes(0);
       expect(result).toBe(
diff --git a/src/utils/notifications.test.ts b/src/utils/notifications.test.ts
index d8c03630..ac22a3a6 100644
--- a/src/utils/notifications.test.ts
+++ b/src/utils/notifications.test.ts
@@ -8,10 +8,7 @@ import { partialMockNotification } from '../__mocks__/partial-mocks';
 import { mockAuth, mockSettings } from '../__mocks__/state-mocks';
 import { defaultSettings } from '../context/App';
 import type { SettingsState } from '../types';
-import {
-  mockGitHubNotifications,
-  mockSingleNotification,
-} from './api/__mocks__/response-mocks';
+import { mockGitHubNotifications } from './api/__mocks__/response-mocks';
 import * as helpers from './helpers';
 import * as notificationsHelpers from './notifications';
 import { filterNotifications } from './notifications';
@@ -98,6 +95,7 @@ describe('utils/notifications.ts', () => {
     jest.spyOn(notificationsHelpers, 'raiseNativeNotification');
     jest.spyOn(notificationsHelpers, 'raiseSoundNotification');
 
+    notificationsHelpers.triggerNativeNotifications([], [], settings, mockAuth);
     notificationsHelpers.triggerNativeNotifications([], [], settings, mockAuth);
 
     expect(notificationsHelpers.raiseNativeNotification).not.toHaveBeenCalled();
@@ -109,15 +107,14 @@ describe('utils/notifications.ts', () => {
 
     const nativeNotification: Notification =
       notificationsHelpers.raiseNativeNotification(
-        [mockSingleNotification],
+        [mockGitHubNotifications[0]],
         mockAuth,
       );
     nativeNotification.onclick(null);
 
     expect(helpers.openInBrowser).toHaveBeenCalledTimes(1);
     expect(helpers.openInBrowser).toHaveBeenLastCalledWith(
-      mockSingleNotification,
-      mockAuth,
+      mockGitHubNotifications[0],
     );
     expect(ipcRenderer.send).toHaveBeenCalledWith('hide-window');
   });
diff --git a/src/utils/remove-notification.test.ts b/src/utils/remove-notification.test.ts
index 28789ac7..d0075cdf 100644
--- a/src/utils/remove-notification.test.ts
+++ b/src/utils/remove-notification.test.ts
@@ -6,7 +6,7 @@ import { removeNotification } from './remove-notification';
 
 describe('utils/remove-notification.ts', () => {
   const notificationId = mockSingleNotification.id;
-  const hostname = mockSingleAccountNotifications[0].hostname;
+  const hostname = mockSingleAccountNotifications[0].account.hostname;
 
   it('should remove a notification if it exists', () => {
     expect(mockSingleAccountNotifications[0].notifications.length).toBe(1);
diff --git a/src/utils/remove-notifications.test.ts b/src/utils/remove-notifications.test.ts
index 181a245f..729b0285 100644
--- a/src/utils/remove-notifications.test.ts
+++ b/src/utils/remove-notifications.test.ts
@@ -7,7 +7,7 @@ import { removeNotifications } from './remove-notifications';
 
 describe('utils/remove-notifications.ts', () => {
   const repoSlug = mockSingleNotification.repository.full_name;
-  const hostname = mockSingleAccountNotifications[0].hostname;
+  const hostname = mockSingleAccountNotifications[0].account.hostname;
 
   it("should remove a repo's notifications - single", () => {
     expect(mockSingleAccountNotifications[0].notifications.length).toBe(1);
diff --git a/src/utils/storage.test.ts b/src/utils/storage.test.ts
index f4cf81cf..42ca9a87 100644
--- a/src/utils/storage.test.ts
+++ b/src/utils/storage.test.ts
@@ -1,16 +1,39 @@
 import { mockSettings } from '../__mocks__/state-mocks';
+import Constants from './constants';
 import { clearState, loadState, saveState } from './storage';
 
 describe('utils/storage.ts', () => {
   it('should load the state from localstorage - existing', () => {
     jest.spyOn(localStorage.__proto__, 'getItem').mockReturnValueOnce(
       JSON.stringify({
-        auth: { token: '123-456' },
+        auth: {
+          accounts: [
+            {
+              hostname: Constants.DEFAULT_AUTH_OPTIONS.hostname,
+              platform: 'GitHub Cloud',
+              method: 'Personal Access Token',
+              token: '123-456',
+              user: null,
+            },
+          ],
+        },
         settings: { theme: 'DARK' },
       }),
     );
     const result = loadState();
-    expect(result.auth.token).toBe('123-456');
+
+    expect(result.auth.accounts).toEqual([
+      {
+        hostname: Constants.DEFAULT_AUTH_OPTIONS.hostname,
+        platform: 'GitHub Cloud',
+        method: 'Personal Access Token',
+        token: '123-456',
+        user: null,
+      },
+    ]);
+    expect(result.auth.token).toBeUndefined();
+    expect(result.auth.enterpriseAccounts).toBeUndefined();
+    expect(result.auth.user).toBeUndefined();
     expect(result.settings.theme).toBe('DARK');
   });
 
@@ -20,6 +43,7 @@ describe('utils/storage.ts', () => {
       .mockReturnValueOnce(JSON.stringify({}));
     const result = loadState();
     expect(result.auth).toBeUndefined();
+    expect(result.auth).toBeUndefined();
     expect(result.settings).toBeUndefined();
   });
 
@@ -27,9 +51,15 @@ describe('utils/storage.ts', () => {
     jest.spyOn(localStorage.__proto__, 'setItem');
     saveState({
       auth: {
-        token: '123-456',
-        enterpriseAccounts: [],
-        user: null,
+        accounts: [
+          {
+            hostname: Constants.DEFAULT_AUTH_OPTIONS.hostname,
+            platform: 'GitHub Cloud',
+            method: 'Personal Access Token',
+            token: '123-456',
+            user: null,
+          },
+        ],
       },
       settings: mockSettings,
     });
diff --git a/src/utils/subject.test.ts b/src/utils/subject.test.ts
index df893078..43419582 100644
--- a/src/utils/subject.test.ts
+++ b/src/utils/subject.test.ts
@@ -5,7 +5,6 @@ import {
   partialMockNotification,
   partialMockUser,
 } from '../__mocks__/partial-mocks';
-import { mockAuth } from '../__mocks__/state-mocks';
 import type {
   Discussion,
   DiscussionAuthor,
@@ -44,10 +43,7 @@ describe('utils/subject.ts', () => {
           type: 'CheckSuite',
         });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'cancelled',
@@ -61,10 +57,7 @@ describe('utils/subject.ts', () => {
           type: 'CheckSuite',
         });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'failure',
@@ -78,10 +71,7 @@ describe('utils/subject.ts', () => {
           type: 'CheckSuite',
         });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'failure',
@@ -95,10 +85,7 @@ describe('utils/subject.ts', () => {
           type: 'CheckSuite',
         });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'skipped',
@@ -112,10 +99,7 @@ describe('utils/subject.ts', () => {
           type: 'CheckSuite',
         });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'success',
@@ -129,10 +113,7 @@ describe('utils/subject.ts', () => {
           type: 'CheckSuite',
         });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toBeNull();
       });
@@ -143,10 +124,7 @@ describe('utils/subject.ts', () => {
           type: 'CheckSuite',
         });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toBeNull();
       });
@@ -171,10 +149,7 @@ describe('utils/subject.ts', () => {
           .get('/repos/gitify-app/notifications-test/comments/141012658')
           .reply(200, { user: mockCommenter });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: null,
@@ -201,10 +176,7 @@ describe('utils/subject.ts', () => {
           )
           .reply(200, { author: mockAuthor });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: null,
@@ -243,10 +215,7 @@ describe('utils/subject.ts', () => {
             },
           });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'ANSWERED',
@@ -271,10 +240,7 @@ describe('utils/subject.ts', () => {
             },
           });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'DUPLICATE',
@@ -299,10 +265,7 @@ describe('utils/subject.ts', () => {
             },
           });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'OPEN',
@@ -327,10 +290,7 @@ describe('utils/subject.ts', () => {
             },
           });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'OUTDATED',
@@ -355,10 +315,7 @@ describe('utils/subject.ts', () => {
             },
           });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'REOPENED',
@@ -383,10 +340,7 @@ describe('utils/subject.ts', () => {
             },
           });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'RESOLVED',
@@ -422,10 +376,7 @@ describe('utils/subject.ts', () => {
           .get('/repos/gitify-app/notifications-test/issues/comments/302888448')
           .reply(200, { user: mockCommenter });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'open',
@@ -447,10 +398,7 @@ describe('utils/subject.ts', () => {
           .get('/repos/gitify-app/notifications-test/issues/comments/302888448')
           .reply(200, { user: mockCommenter });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'closed',
@@ -476,10 +424,7 @@ describe('utils/subject.ts', () => {
           .get('/repos/gitify-app/notifications-test/issues/comments/302888448')
           .reply(200, { user: mockCommenter });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'completed',
@@ -505,10 +450,7 @@ describe('utils/subject.ts', () => {
           .get('/repos/gitify-app/notifications-test/issues/comments/302888448')
           .reply(200, { user: mockCommenter });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'not_planned',
@@ -534,10 +476,7 @@ describe('utils/subject.ts', () => {
           .get('/repos/gitify-app/notifications-test/issues/comments/302888448')
           .reply(200, { user: mockCommenter });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'reopened',
@@ -562,10 +501,7 @@ describe('utils/subject.ts', () => {
             user: mockAuthor,
           });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'open',
@@ -610,10 +546,7 @@ describe('utils/subject.ts', () => {
           .get('/repos/gitify-app/notifications-test/pulls/1/reviews')
           .reply(200, []);
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'closed',
@@ -645,10 +578,7 @@ describe('utils/subject.ts', () => {
           .get('/repos/gitify-app/notifications-test/pulls/1/reviews')
           .reply(200, []);
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'draft',
@@ -680,10 +610,7 @@ describe('utils/subject.ts', () => {
           .get('/repos/gitify-app/notifications-test/pulls/1/reviews')
           .reply(200, []);
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'merged',
@@ -715,10 +642,7 @@ describe('utils/subject.ts', () => {
           .get('/repos/gitify-app/notifications-test/pulls/1/reviews')
           .reply(200, []);
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'open',
@@ -749,10 +673,7 @@ describe('utils/subject.ts', () => {
           .get('/repos/gitify-app/notifications-test/pulls/1/reviews')
           .reply(200, []);
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'open',
@@ -782,10 +703,7 @@ describe('utils/subject.ts', () => {
           .get('/repos/gitify-app/notifications-test/pulls/1/reviews')
           .reply(200, []);
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'open',
@@ -830,10 +748,7 @@ describe('utils/subject.ts', () => {
               },
             ]);
 
-          const result = await getLatestReviewForReviewers(
-            mockNotification,
-            mockAuth.token,
-          );
+          const result = await getLatestReviewForReviewers(mockNotification);
 
           expect(result).toEqual([
             { state: 'APPROVED', users: ['reviewer-3', 'reviewer-1'] },
@@ -846,10 +761,7 @@ describe('utils/subject.ts', () => {
             .get('/repos/gitify-app/notifications-test/pulls/1/reviews')
             .reply(200, []);
 
-          const result = await getLatestReviewForReviewers(
-            mockNotification,
-            mockAuth.token,
-          );
+          const result = await getLatestReviewForReviewers(mockNotification);
 
           expect(result).toBeNull();
         });
@@ -857,10 +769,7 @@ describe('utils/subject.ts', () => {
         it('returns null when not a PR notification', async () => {
           mockNotification.subject.type = 'Issue';
 
-          const result = await getLatestReviewForReviewers(
-            mockNotification,
-            mockAuth.token,
-          );
+          const result = await getLatestReviewForReviewers(mockNotification);
 
           expect(result).toBeNull();
         });
@@ -881,10 +790,7 @@ describe('utils/subject.ts', () => {
           .get('/repos/gitify-app/notifications-test/releases/1')
           .reply(200, { author: mockAuthor });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: null,
@@ -905,10 +811,7 @@ describe('utils/subject.ts', () => {
           type: 'WorkflowRun',
         });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toEqual({
           state: 'waiting',
@@ -923,10 +826,7 @@ describe('utils/subject.ts', () => {
           type: 'WorkflowRun',
         });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toBeNull();
       });
@@ -937,10 +837,7 @@ describe('utils/subject.ts', () => {
           type: 'WorkflowRun',
         });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toBeNull();
       });
@@ -954,10 +851,7 @@ describe('utils/subject.ts', () => {
           type: 'RepositoryInvitation',
         });
 
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAuth.token,
-        );
+        const result = await getGitifySubjectDetails(mockNotification);
 
         expect(result).toBeNull();
       });
@@ -980,7 +874,7 @@ describe('utils/subject.ts', () => {
           .get('/repos/gitify-app/notifications-test/issues/1')
           .replyWithError(mockError);
 
-        await getGitifySubjectDetails(mockNotification, mockAuth.token);
+        await getGitifySubjectDetails(mockNotification);
 
         expect(consoleErrorSpy).toHaveBeenCalledWith(
           'Error occurred while fetching details for Issue notification: This issue will throw an error',