From 373b12bcb8b769af9bdd2daaf1a3d90c1705a3a5 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Mon, 27 Jan 2025 06:35:37 -0500 Subject: [PATCH 01/13] wip Signed-off-by: Adam Setch --- src/main/main.ts | 23 ++++- src/renderer/context/App.tsx | 22 +++-- src/renderer/routes/Accounts.tsx | 22 ++--- src/renderer/routes/Login.tsx | 30 ++++--- src/renderer/utils/auth/utils.ts | 142 ++++++++++++++++--------------- 5 files changed, 133 insertions(+), 106 deletions(-) diff --git a/src/main/main.ts b/src/main/main.ts index 0f307a007..53ef10579 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -37,11 +37,14 @@ const mb = menubar({ const menuBuilder = new MenuBuilder(mb); const contextMenu = menuBuilder.buildMenu(); -/** - * Electron Auto Updater only supports macOS and Windows - * https://github.com/electron/update-electron-app - */ +// Register your app as the handler for a custom protocol +app.setAsDefaultProtocolClient('gitify'); + if (isMacOS() || isWindows()) { + /** + * Electron Auto Updater only supports macOS and Windows + * https://github.com/electron/update-electron-app + */ const updater = new Updater(mb, menuBuilder); updater.initialize(); } @@ -186,3 +189,15 @@ app.whenReady().then(async () => { app.setLoginItemSettings(settings); }); }); + +app.on('open-url', (event, url) => { + event.preventDefault(); + const code = new URL(url).searchParams.get('code'); // Extract the authorization code + console.log('Authorization Code:', code); + + if (code) { + // Exchange the code for an access token + mb.window.webContents.send(namespacedEvent('auth-code'), code); + // exchangeCodeForToken(code); + } +}); diff --git a/src/renderer/context/App.tsx b/src/renderer/context/App.tsx index a9764ead3..f49c39088 100644 --- a/src/renderer/context/App.tsx +++ b/src/renderer/context/App.tsx @@ -16,6 +16,7 @@ import { useNotifications } from '../hooks/useNotifications'; import { type Account, type AccountNotifications, + type AuthCode, type AuthState, type GitifyError, GroupBy, @@ -108,7 +109,7 @@ export const defaultSettings: SettingsState = { interface AppContextState { auth: AuthState; isLoggedIn: boolean; - loginWithGitHubApp: () => void; + loginWithGitHubApp: (authCode: AuthCode) => void; loginWithOAuthApp: (data: LoginOAuthAppOptions) => void; loginWithPersonalAccessToken: (data: LoginPersonalAccessTokenOptions) => void; logoutFromAccount: (account: Account) => void; @@ -231,14 +232,17 @@ export const AppProvider = ({ children }: { children: ReactNode }) => { return hasAccounts(auth); }, [auth]); - const loginWithGitHubApp = useCallback(async () => { - const { authCode } = await authGitHub(); - const { token } = await getToken(authCode); - const hostname = Constants.DEFAULT_AUTH_OPTIONS.hostname; - const updatedAuth = await addAccount(auth, 'GitHub App', token, hostname); - setAuth(updatedAuth); - saveState({ auth: updatedAuth, settings }); - }, [auth, settings]); + const loginWithGitHubApp = useCallback( + async (authCode: AuthCode) => { + // const { authCode } = await authGitHub(); + const { token } = await getToken(authCode); + const hostname = Constants.DEFAULT_AUTH_OPTIONS.hostname; + const updatedAuth = await addAccount(auth, 'GitHub App', token, hostname); + setAuth(updatedAuth); + saveState({ auth: updatedAuth, settings }); + }, + [auth, settings], + ); const loginWithOAuthApp = useCallback( async (data: LoginOAuthAppOptions) => { diff --git a/src/renderer/routes/Accounts.tsx b/src/renderer/routes/Accounts.tsx index 50d542851..1be751a44 100644 --- a/src/renderer/routes/Accounts.tsx +++ b/src/renderer/routes/Accounts.tsx @@ -22,20 +22,23 @@ import { Text, } from '@primer/react'; -import { logError } from '../../shared/logger'; import { AvatarWithFallback } from '../components/avatars/AvatarWithFallback'; import { Contents } from '../components/layout/Contents'; import { Page } from '../components/layout/Page'; import { Footer } from '../components/primitives/Footer'; import { Header } from '../components/primitives/Header'; import { AppContext } from '../context/App'; -import { type Account, Size } from '../types'; +import { type Account, type Link, Size } from '../types'; import { formatRequiredScopes, getAccountUUID, refreshAccount, } from '../utils/auth/utils'; -import { updateTrayIcon, updateTrayTitle } from '../utils/comms'; +import { + openExternalLink, + updateTrayIcon, + updateTrayTitle, +} from '../utils/comms'; import { getAuthMethodIcon, getPlatformIcon } from '../utils/icons'; import { openAccountProfile, @@ -45,8 +48,7 @@ import { import { saveState } from '../utils/storage'; export const AccountsRoute: FC = () => { - const { auth, settings, loginWithGitHubApp, logoutFromAccount } = - useContext(AppContext); + const { auth, settings, logoutFromAccount } = useContext(AppContext); const navigate = useNavigate(); const logoutAccount = useCallback( @@ -65,14 +67,6 @@ export const AccountsRoute: FC = () => { navigate('/accounts', { replace: true }); }, []); - const loginWithGitHub = useCallback(async () => { - try { - await loginWithGitHubApp(); - } catch (err) { - logError('loginWithGitHub', 'failed to login with GitHub', err); - } - }, []); - const loginWithPersonalAccessToken = useCallback(() => { return navigate('/login-personal-access-token', { replace: true }); }, []); @@ -237,7 +231,7 @@ export const AccountsRoute: FC = () => { loginWithGitHub()} + onSelect={() => openExternalLink('https://github.com' as Link)} data-testid="account-add-github" > diff --git a/src/renderer/routes/Login.tsx b/src/renderer/routes/Login.tsx index 489518cba..26129123c 100644 --- a/src/renderer/routes/Login.tsx +++ b/src/renderer/routes/Login.tsx @@ -1,14 +1,15 @@ import { KeyIcon, MarkGithubIcon, PersonIcon } from '@primer/octicons-react'; import { Button, Heading, Stack, Text } from '@primer/react'; -import { type FC, useCallback, useContext, useEffect } from 'react'; +import { type FC, useContext, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { logError } from '../../shared/logger'; +import { ipcRenderer } from 'electron'; +import { namespacedEvent } from '../../shared/events'; import { LogoIcon } from '../components/icons/LogoIcon'; import { Centered } from '../components/layout/Centered'; import { AppContext } from '../context/App'; -import { Size } from '../types'; -import { showWindow } from '../utils/comms'; +import { type AuthCode, type Link, Size } from '../types'; +import { openExternalLink, showWindow } from '../utils/comms'; export const LoginRoute: FC = () => { const navigate = useNavigate(); @@ -21,14 +22,21 @@ export const LoginRoute: FC = () => { } }, [isLoggedIn]); - const loginUser = useCallback(() => { - try { - loginWithGitHubApp(); - } catch (err) { - logError('loginWithGitHubApp', 'failed to login with GitHub', err); - } + useEffect(() => { + ipcRenderer.on(namespacedEvent('auth-code'), (_, authCode: AuthCode) => { + console.log('RENDER AUTH CODE', authCode); + loginWithGitHubApp(authCode); + }); }, [loginWithGitHubApp]); + // const loginUser = useCallback(() => { + // try { + // loginWithGitHubApp(); + // } catch (err) { + // logError('loginWithGitHubApp', 'failed to login with GitHub', err); + // } + // }, [loginWithGitHubApp]); + return ( @@ -45,7 +53,7 @@ export const LoginRoute: FC = () => { {' '} + {' '} for more details. } diff --git a/src/renderer/routes/Accounts.tsx b/src/renderer/routes/Accounts.tsx index 50d542851..7f9e8f63b 100644 --- a/src/renderer/routes/Accounts.tsx +++ b/src/renderer/routes/Accounts.tsx @@ -107,7 +107,6 @@ export const AccountsRoute: FC = () => { title="Open account profile" onClick={() => openAccountProfile(account)} data-testid="account-profile" - className="pb-2" > { - + - + diff --git a/src/renderer/routes/__snapshots__/Accounts.test.tsx.snap b/src/renderer/routes/__snapshots__/Accounts.test.tsx.snap index 5658d01f7..c1a1f0fc0 100644 --- a/src/renderer/routes/__snapshots__/Accounts.test.tsx.snap +++ b/src/renderer/routes/__snapshots__/Accounts.test.tsx.snap @@ -111,7 +111,7 @@ exports[`renderer/routes/Accounts.tsx Account interactions should render with PA > - + @@ -473,7 +473,7 @@ exports[`renderer/routes/Accounts.tsx Account interactions should render with PA > - + @@ -835,7 +835,7 @@ exports[`renderer/routes/Accounts.tsx Account interactions should render with PA > - + @@ -1348,7 +1348,7 @@ exports[`renderer/routes/Accounts.tsx Account interactions should set account as > - + @@ -1710,7 +1710,7 @@ exports[`renderer/routes/Accounts.tsx Account interactions should set account as > - + @@ -2072,7 +2072,7 @@ exports[`renderer/routes/Accounts.tsx Account interactions should set account as > - + @@ -2585,7 +2585,7 @@ exports[`renderer/routes/Accounts.tsx General should render itself & its childre > - + @@ -2947,7 +2947,7 @@ exports[`renderer/routes/Accounts.tsx General should render itself & its childre > - + @@ -3309,7 +3309,7 @@ exports[`renderer/routes/Accounts.tsx General should render itself & its childre > - + From 9eb07ce02652e6b2fe03c9fc6735d31a047dd90b Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Tue, 28 Jan 2025 16:44:07 -0500 Subject: [PATCH 11/13] fix add account error Signed-off-by: Adam Setch --- .../settings/AppearanceSettings.tsx | 4 +- src/renderer/routes/Accounts.tsx | 48 ++++++++++++------- .../__snapshots__/Settings.test.tsx.snap | 24 ---------- src/renderer/utils/auth/utils.ts | 20 +++++++- 4 files changed, 52 insertions(+), 44 deletions(-) diff --git a/src/renderer/components/settings/AppearanceSettings.tsx b/src/renderer/components/settings/AppearanceSettings.tsx index 5d58ed82d..9d50ef66c 100644 --- a/src/renderer/components/settings/AppearanceSettings.tsx +++ b/src/renderer/components/settings/AppearanceSettings.tsx @@ -260,8 +260,8 @@ export const AppearanceSettings: FC = () => { updateSetting('showAccountHeader', evt.target.checked) } diff --git a/src/renderer/routes/Accounts.tsx b/src/renderer/routes/Accounts.tsx index 7f9e8f63b..c6afe1abd 100644 --- a/src/renderer/routes/Accounts.tsx +++ b/src/renderer/routes/Accounts.tsx @@ -49,6 +49,10 @@ export const AccountsRoute: FC = () => { useContext(AppContext); const navigate = useNavigate(); + const [loadingStates, setLoadingStates] = useState>( + {}, + ); + const logoutAccount = useCallback( (account: Account) => { logoutFromAccount(account); @@ -65,6 +69,29 @@ export const AccountsRoute: FC = () => { navigate('/accounts', { replace: true }); }, []); + const handleRefresh = useCallback(async (account: Account) => { + const accountUUID = getAccountUUID(account); + + setLoadingStates((prev) => ({ + ...prev, + [accountUUID]: true, + })); + + await refreshAccount(account); + navigate('/accounts', { replace: true }); + + /** + * Typically the above refresh API call completes very quickly, + * so we add an brief artificial delay to allow the icon to spin a few times + */ + setTimeout(() => { + setLoadingStates((prev) => ({ + ...prev, + [accountUUID]: false, + })); + }, 500); + }, []); + const loginWithGitHub = useCallback(async () => { try { await loginWithGitHubApp(); @@ -89,11 +116,11 @@ export const AccountsRoute: FC = () => { {auth.accounts.map((account, i) => { const AuthMethodIcon = getAuthMethodIcon(account.method); const PlatformIcon = getPlatformIcon(account.platform); - const [isRefreshingAccount, setIsRefreshingAccount] = useState(false); + const accountUUID = getAccountUUID(account); return ( { { - setIsRefreshingAccount(true); - - await refreshAccount(account); - navigate('/accounts', { replace: true }); - - /** - * Typically the above refresh API call completes very quickly, - * so we add an brief artificial delay to allow the icon to spin a few times - */ - setTimeout(() => { - setIsRefreshingAccount(false); - }, 500); - }} + onClick={() => handleRefresh(account)} size="small" - loading={isRefreshingAccount} + loading={loadingStates[accountUUID] || false} data-testid="account-refresh" /> diff --git a/src/renderer/routes/__snapshots__/Settings.test.tsx.snap b/src/renderer/routes/__snapshots__/Settings.test.tsx.snap index e47d7138d..ea3684d94 100644 --- a/src/renderer/routes/__snapshots__/Settings.test.tsx.snap +++ b/src/renderer/routes/__snapshots__/Settings.test.tsx.snap @@ -518,30 +518,6 @@ exports[`renderer/routes/Settings.tsx should render itself & its children 1`] = -
- - -
{ + const accountList = auth.accounts; + let newAccount = { hostname: hostname, method: method, @@ -116,9 +118,23 @@ export async function addAccount( } as Account; newAccount = await refreshAccount(newAccount); + const newAccountUUID = getAccountUUID(newAccount); + + const existingAccount = accountList.find( + (a) => getAccountUUID(a) === newAccountUUID, + ); + + if (existingAccount) { + logWarn( + 'addAccount', + `account for user ${newAccount.user.login} already exists`, + ); + } else { + accountList.push(newAccount); + } return { - accounts: [...auth.accounts, newAccount], + accounts: accountList, }; } @@ -136,6 +152,8 @@ export async function refreshAccount(account: Account): Promise { try { const res = await getAuthenticatedUser(account.hostname, account.token); + // console.log('ADAM RESPONSE', JSON.stringify(res, null, 2)); + // Refresh user data account.user = { id: res.data.id, From b2ccf2c21d4056c1afa793c3493650900feb6ba2 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Tue, 28 Jan 2025 18:05:18 -0500 Subject: [PATCH 12/13] feat: use system browser for oauth flow Signed-off-by: Adam Setch --- src/main/main.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/main/main.ts b/src/main/main.ts index 02669f3d6..4ce7e36de 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -56,13 +56,6 @@ app.whenReady().then(async () => { mb.on('ready', () => { mb.app.setAppUserModelId(APPLICATION.ID); - /** - * TODO: Remove @electron/remote use - see #650 - * GitHub OAuth 2 Login Flows - Enable Remote Browser Window Launch - */ - require('@electron/remote/main').initialize(); - require('@electron/remote/main').enable(mb.window.webContents); - // Tray configuration mb.tray.setToolTip(APPLICATION.NAME); mb.tray.setIgnoreDoubleClickEvents(true); From 2d8847effd95991bb6bc97c2e49b7236b22dba08 Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Wed, 29 Jan 2025 08:02:08 -0500 Subject: [PATCH 13/13] feat: use system browser for oauth flow Signed-off-by: Adam Setch --- src/renderer/utils/auth/utils.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/renderer/utils/auth/utils.ts b/src/renderer/utils/auth/utils.ts index 44556bf36..004974040 100644 --- a/src/renderer/utils/auth/utils.ts +++ b/src/renderer/utils/auth/utils.ts @@ -120,11 +120,11 @@ export async function addAccount( newAccount = await refreshAccount(newAccount); const newAccountUUID = getAccountUUID(newAccount); - const existingAccount = accountList.find( + const accountAlreadyExists = accountList.some( (a) => getAccountUUID(a) === newAccountUUID, ); - if (existingAccount) { + if (accountAlreadyExists) { logWarn( 'addAccount', `account for user ${newAccount.user.login} already exists`, @@ -152,8 +152,6 @@ export async function refreshAccount(account: Account): Promise { try { const res = await getAuthenticatedUser(account.hostname, account.token); - // console.log('ADAM RESPONSE', JSON.stringify(res, null, 2)); - // Refresh user data account.user = { id: res.data.id,