Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ const config: Config = {
},
// Allow transforming specific ESM packages in node_modules that ship untranspiled ESM.
// @primer/* libraries rely on lit and @lit-labs/react internally for some components.
// @octokit/* libraries rely on universal-user-agent internally.
// We also include GitHub web components that ship ESM-only builds.
transformIgnorePatterns: [
'node_modules/(?!(?:@primer/react|@primer/primitives|@primer/octicons-react|@lit-labs/react|lit|@github/relative-time-element|@github/tab-container-element)/)',
'node_modules/(?!(?:@primer/react|@primer/primitives|@primer/octicons-react|@lit-labs/react|lit|@github/relative-time-element|@github/tab-container-element|@octokit/oauth-methods|@octokit/oauth-authorization-url|@octokit/request|@octokit/request-error|@octokit/endpoint|universal-user-agent)/)',
],
moduleNameMapper: {
// Force CommonJS build for http adapter to be available.
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@
"@electron/notarize": "3.1.1",
"@graphql-codegen/cli": "6.1.1",
"@graphql-codegen/schema-ast": "5.0.0",
"@octokit/oauth-methods": "6.0.2",
"@octokit/openapi-types": "27.0.0",
"@octokit/request": "10.0.7",
"@parcel/watcher": "2.5.4",
"@primer/css": "22.1.0",
"@primer/octicons-react": "19.21.2",
Expand Down
69 changes: 69 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion src/main/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ export default class MenuBuilder {
{
label: 'Visit Repository',
click: () => {
shell.openExternal(`https://github.com/${APPLICATION.REPO_SLUG}`);
shell.openExternal(
`${APPLICATION.GITHUB_BASE_URL}/${APPLICATION.REPO_SLUG}`,
);
},
},
{
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ClientID, ClientSecret, Hostname, Link } from './types';
import type { LoginOAuthAppOptions } from './utils/auth/types';

export const Constants = {
STORAGE_KEY: 'gitify-storage',
Expand All @@ -13,7 +14,7 @@ export const Constants = {
hostname: 'github.com' as Hostname,
clientId: process.env.OAUTH_CLIENT_ID as ClientID,
clientSecret: process.env.OAUTH_CLIENT_SECRET as ClientSecret,
},
} satisfies LoginOAuthAppOptions,

GITHUB_API_BASE_URL: 'https://api.github.com',
GITHUB_API_GRAPHQL_URL: 'https://api.github.com/graphql',
Expand Down
31 changes: 24 additions & 7 deletions src/renderer/context/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ import type {
import { headNotifications } from '../utils/api/client';
import {
addAccount,
authGitHub,
exchangeAuthCodeForAccessToken,
getAccountUUID,
getToken,
hasAccounts,
performGitHubOAuth,
refreshAccount,
removeAccount,
} from '../utils/auth/utils';
Expand Down Expand Up @@ -395,28 +395,45 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
return hasAccounts(auth);
}, [auth]);

/**
* Login with GitHub App.
*
* Note: although we call this "Login with GitHub App", this function actually
* authenticates via a predefined "Gitify" GitHub OAuth App.
*/
const loginWithGitHubApp = useCallback(async () => {
const { authCode } = await authGitHub();
const { token } = await getToken(authCode);
const { authCode } = await performGitHubOAuth();
const token = await exchangeAuthCodeForAccessToken(authCode);
const hostname = Constants.DEFAULT_AUTH_OPTIONS.hostname;

const updatedAuth = await addAccount(auth, 'GitHub App', token, hostname);

persistAuth(updatedAuth);
}, [auth, persistAuth]);

/**
* Login with custom GitHub OAuth App.
*/
const loginWithOAuthApp = useCallback(
async (data: LoginOAuthAppOptions) => {
const { authOptions, authCode } = await authGitHub(data);
const { token, hostname } = await getToken(authCode, authOptions);
const { authOptions, authCode } = await performGitHubOAuth(data);
const token = await exchangeAuthCodeForAccessToken(authCode, authOptions);

const updatedAuth = await addAccount(auth, 'OAuth App', token, hostname);
const updatedAuth = await addAccount(
auth,
'OAuth App',
token,
authOptions.hostname,
);

persistAuth(updatedAuth);
},
[auth, persistAuth],
);

/**
* Login with Personal Access Token (PAT).
*/
const loginWithPersonalAccessToken = useCallback(
async ({ token, hostname }: LoginPersonalAccessTokenOptions) => {
const encryptedToken = (await encryptValue(token)) as Token;
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/utils/api/graphql/generated/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ type Documents = {
"query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n ...IssueDetails\n }\n }\n}\n\nfragment IssueDetails on Issue {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n}": typeof types.FetchIssueByNumberDocument,
"query FetchMergedDetailsTemplate($ownerINDEX: String!, $nameINDEX: String!, $numberINDEX: Int!, $isDiscussionNotificationINDEX: Boolean!, $isIssueNotificationINDEX: Boolean!, $isPullRequestNotificationINDEX: Boolean!, $lastComments: Int, $lastThreadedComments: Int, $lastReplies: Int, $lastReviews: Int, $firstLabels: Int, $firstClosingIssues: Int, $includeIsAnswered: Boolean!) {\n ...MergedDetailsQueryTemplate\n}\n\nfragment MergedDetailsQueryTemplate on Query {\n repository(owner: $ownerINDEX, name: $nameINDEX) {\n discussion(number: $numberINDEX) @include(if: $isDiscussionNotificationINDEX) {\n ...DiscussionDetails\n }\n issue(number: $numberINDEX) @include(if: $isIssueNotificationINDEX) {\n ...IssueDetails\n }\n pullRequest(number: $numberINDEX) @include(if: $isPullRequestNotificationINDEX) {\n ...PullRequestDetails\n }\n }\n}": typeof types.FetchMergedDetailsTemplateDocument,
"query FetchPullRequestByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int, $lastComments: Int, $lastReviews: Int, $firstClosingIssues: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n ...PullRequestDetails\n }\n }\n}\n\nfragment PullRequestDetails on PullRequest {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: $lastReviews) {\n totalCount\n nodes {\n ...PullRequestReviewFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: $firstClosingIssues) {\n nodes {\n number\n }\n }\n}\n\nfragment PullRequestReviewFields on PullRequestReview {\n state\n author {\n login\n }\n}": typeof types.FetchPullRequestByNumberDocument,
"query FetchAuthenticatedUserDetails {\n viewer {\n id\n name\n login\n avatarUrl\n }\n}": typeof types.FetchAuthenticatedUserDetailsDocument,
"query FetchAuthenticatedUserDetails {\n viewer {\n id\n name\n login\n avatar: avatarUrl\n }\n}": typeof types.FetchAuthenticatedUserDetailsDocument,
};
const documents: Documents = {
"fragment AuthorFields on Actor {\n login\n htmlUrl: url\n avatarUrl: avatarUrl\n type: __typename\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.AuthorFieldsFragmentDoc,
"query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastThreadedComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n ...DiscussionDetails\n }\n }\n}\n\nfragment DiscussionDetails on Discussion {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastThreadedComments) {\n totalCount\n nodes {\n ...DiscussionCommentFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}\n\nfragment DiscussionCommentFields on DiscussionComment {\n ...CommentFields\n replies(last: $lastReplies) {\n totalCount\n nodes {\n ...CommentFields\n }\n }\n}": types.FetchDiscussionByNumberDocument,
"query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n ...IssueDetails\n }\n }\n}\n\nfragment IssueDetails on Issue {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n}": types.FetchIssueByNumberDocument,
"query FetchMergedDetailsTemplate($ownerINDEX: String!, $nameINDEX: String!, $numberINDEX: Int!, $isDiscussionNotificationINDEX: Boolean!, $isIssueNotificationINDEX: Boolean!, $isPullRequestNotificationINDEX: Boolean!, $lastComments: Int, $lastThreadedComments: Int, $lastReplies: Int, $lastReviews: Int, $firstLabels: Int, $firstClosingIssues: Int, $includeIsAnswered: Boolean!) {\n ...MergedDetailsQueryTemplate\n}\n\nfragment MergedDetailsQueryTemplate on Query {\n repository(owner: $ownerINDEX, name: $nameINDEX) {\n discussion(number: $numberINDEX) @include(if: $isDiscussionNotificationINDEX) {\n ...DiscussionDetails\n }\n issue(number: $numberINDEX) @include(if: $isIssueNotificationINDEX) {\n ...IssueDetails\n }\n pullRequest(number: $numberINDEX) @include(if: $isPullRequestNotificationINDEX) {\n ...PullRequestDetails\n }\n }\n}": types.FetchMergedDetailsTemplateDocument,
"query FetchPullRequestByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int, $lastComments: Int, $lastReviews: Int, $firstClosingIssues: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n ...PullRequestDetails\n }\n }\n}\n\nfragment PullRequestDetails on PullRequest {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: $lastReviews) {\n totalCount\n nodes {\n ...PullRequestReviewFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: $firstClosingIssues) {\n nodes {\n number\n }\n }\n}\n\nfragment PullRequestReviewFields on PullRequestReview {\n state\n author {\n login\n }\n}": types.FetchPullRequestByNumberDocument,
"query FetchAuthenticatedUserDetails {\n viewer {\n id\n name\n login\n avatarUrl\n }\n}": types.FetchAuthenticatedUserDetailsDocument,
"query FetchAuthenticatedUserDetails {\n viewer {\n id\n name\n login\n avatar: avatarUrl\n }\n}": types.FetchAuthenticatedUserDetailsDocument,
};

/**
Expand All @@ -54,7 +54,7 @@ export function graphql(source: "query FetchPullRequestByNumber($owner: String!,
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "query FetchAuthenticatedUserDetails {\n viewer {\n id\n name\n login\n avatarUrl\n }\n}"): typeof import('./graphql').FetchAuthenticatedUserDetailsDocument;
export function graphql(source: "query FetchAuthenticatedUserDetails {\n viewer {\n id\n name\n login\n avatar: avatarUrl\n }\n}"): typeof import('./graphql').FetchAuthenticatedUserDetailsDocument;


export function graphql(source: string) {
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/utils/api/graphql/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35855,7 +35855,7 @@ export type PullRequestReviewFieldsFragment = { __typename?: 'PullRequestReview'
export type FetchAuthenticatedUserDetailsQueryVariables = Exact<{ [key: string]: never; }>;


export type FetchAuthenticatedUserDetailsQuery = { __typename?: 'Query', viewer: { __typename?: 'User', id: string, name?: string | null, login: string, avatarUrl: any } };
export type FetchAuthenticatedUserDetailsQuery = { __typename?: 'Query', viewer: { __typename?: 'User', id: string, name?: string | null, login: string, avatar: any } };

export class TypedDocumentString<TResult, TVariables>
extends String
Expand Down Expand Up @@ -36528,7 +36528,7 @@ export const FetchAuthenticatedUserDetailsDocument = new TypedDocumentString(`
id
name
login
avatarUrl
avatar: avatarUrl
}
}
`) as unknown as TypedDocumentString<FetchAuthenticatedUserDetailsQuery, FetchAuthenticatedUserDetailsQueryVariables>;
2 changes: 1 addition & 1 deletion src/renderer/utils/api/graphql/user.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ query FetchAuthenticatedUserDetails {
id
name
login
avatarUrl
avatar: avatarUrl
}
}
28 changes: 0 additions & 28 deletions src/renderer/utils/api/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type { Link } from '../../types';

import { FetchAuthenticatedUserDetailsDocument } from './graphql/generated/graphql';
import {
apiRequest,
apiRequestAuth,
getHeaders,
performGraphQLRequest,
Expand All @@ -29,33 +28,6 @@ describe('renderer/utils/api/request.ts', () => {
jest.clearAllMocks();
});

describe('apiRequest', () => {
it('should make a request with the correct parameters', async () => {
const data = { key: 'value' };

await apiRequest(url, method, data);

expect(axios).toHaveBeenCalledWith({
method,
url,
data,
headers: mockNoAuthHeaders,
});
});

it('should make a request with the correct parameters and default data', async () => {
const data = {};
await apiRequest(url, method);

expect(axios).toHaveBeenCalledWith({
method,
url,
data,
headers: mockNoAuthHeaders,
});
});
});

describe('apiRequestAuth', () => {
afterEach(() => {
jest.clearAllMocks();
Expand Down
18 changes: 0 additions & 18 deletions src/renderer/utils/api/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,6 @@ export type ExecutionResultWithHeaders<T> = ExecutionResult<T> & {
headers: Record<string, string>;
};

/**
* Perform an unauthenticated API request
*
* @param url
* @param method
* @param data
* @returns
*/
export async function apiRequest(
url: Link,
method: Method,
data = {},
): Promise<AxiosPromise | null> {
const headers = await getHeaders(url);

return axios({ method, url, data, headers });
}

/**
* Perform an authenticated API request
*
Expand Down
13 changes: 13 additions & 0 deletions src/renderer/utils/api/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,24 @@ import type { Hostname } from '../../types';

import {
getGitHubAPIBaseUrl,
getGitHubAuthBaseUrl,
getGitHubGraphQLUrl,
getNextURLFromLinkHeader,
} from './utils';

describe('renderer/utils/api/utils.ts', () => {
describe('getGitHubAuthBaseUrl', () => {
it('should generate a GitHub Auth url - non enterprise', () => {
const result = getGitHubAuthBaseUrl('github.com' as Hostname);
expect(result.toString()).toBe('https://github.com/');
});

it('should generate a GitHub Auth url - enterprise', () => {
const result = getGitHubAuthBaseUrl('github.gitify.io' as Hostname);
expect(result.toString()).toBe('https://github.gitify.io/api/v3/');
});
});

describe('getGitHubAPIBaseUrl', () => {
it('should generate a GitHub API url - non enterprise', () => {
const result = getGitHubAPIBaseUrl('github.com' as Hostname);
Expand Down
Loading