From 66b8340ec257d3983ede4aa2da4c64a664ad916b Mon Sep 17 00:00:00 2001 From: Thomas Lombart Date: Wed, 31 Jan 2024 10:13:19 +0100 Subject: [PATCH 1/3] OAuth utils: Fix no view props --- package.json | 2 +- src/oauth/withAccessToken.tsx | 33 ++++++++++++--------- tests/package-lock.json | 2 +- tests/package.json | 40 +++++++++++++++++++++++++- tests/src/oauth-no-view-with-props.tsx | 16 +++++++++++ tests/src/oauth-with-props.tsx | 12 ++++++++ 6 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 tests/src/oauth-no-view-with-props.tsx create mode 100644 tests/src/oauth-with-props.tsx diff --git a/package.json b/package.json index aa4e97f..ff42c01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@raycast/utils", - "version": "1.11.1", + "version": "1.11.2", "description": "Set of utilities to streamline building Raycast extensions", "author": "Raycast Technologies Ltd.", "homepage": "https://developers.raycast.com/utils-reference", diff --git a/src/oauth/withAccessToken.tsx b/src/oauth/withAccessToken.tsx index a344bc3..114b473 100644 --- a/src/oauth/withAccessToken.tsx +++ b/src/oauth/withAccessToken.tsx @@ -40,6 +40,8 @@ type WithAccessTokenParameters = { onAuthorize?: (params: OnAuthorizeParams) => void; }; +export type WithAccessTokenComponentOrFn = ((params: T) => Promise | void) | React.ComponentType; + /** * Higher-order component to wrap a given component or function and set an access token in a shared global variable. * @@ -67,24 +69,29 @@ type WithAccessTokenParameters = { * @param {string} options.personalAccessToken - An optional personal access token. * @returns {React.ComponentType} The wrapped component. */ -export function withAccessToken( +export function withAccessToken( options: WithAccessTokenParameters, -): Promise | void) | React.ComponentType>( +): >( fnOrComponent: U, -) => U extends () => Promise | void ? Promise : React.FunctionComponent; +) => U extends (props: T) => Promise | void ? Promise : React.FunctionComponent; export function withAccessToken(options: WithAccessTokenParameters) { if (environment.commandMode === "no-view") { - return async (fn: () => Promise | (() => void)) => { - if (!token) { - token = options.personalAccessToken ?? (await options.authorize()); - type = options.personalAccessToken ? "personal" : "oauth"; - const idToken = (await options.client?.getTokens())?.idToken; - - if (options.onAuthorize) { - await Promise.resolve(options.onAuthorize({ token, type, idToken })); + return (fn: (props: T) => Promise | (() => void)) => { + const noViewFn = async (props: T) => { + if (!token) { + token = options.personalAccessToken ?? (await options.authorize()); + type = options.personalAccessToken ? "personal" : "oauth"; + const idToken = (await options.client?.getTokens())?.idToken; + + if (options.onAuthorize) { + await Promise.resolve(options.onAuthorize({ token, type, idToken })); + } } - } - return fn(); + + return fn(props); + }; + + return noViewFn; }; } diff --git a/tests/package-lock.json b/tests/package-lock.json index 3db5943..16797df 100644 --- a/tests/package-lock.json +++ b/tests/package-lock.json @@ -22,7 +22,7 @@ }, "..": { "name": "@raycast/utils", - "version": "1.10.1", + "version": "1.11.1", "license": "MIT", "dependencies": { "content-type": "^1.0.5", diff --git a/tests/package.json b/tests/package.json index 46f0e0a..5ee6583 100644 --- a/tests/package.json +++ b/tests/package.json @@ -113,12 +113,50 @@ "description": "Utils Smoke Tests", "mode": "view" }, + { + "name": "oauth-with-props", + "title": "OAuth with Props", + "subtitle": "Utils Smoke Tests", + "description": "Utils Smoke Tests", + "mode": "view", + "arguments": [ + { + "name": "text", + "placeholder": "Text", + "type": "text", + "required": true + } + ] + }, { "name": "oauth-no-view", "title": "OAuth No View", "subtitle": "Utils Smoke Tests", "description": "Utils Smoke Tests", - "mode": "no-view" + "mode": "no-view", + "arguments": [ + { + "name": "comment", + "placeholder": "Comment", + "type": "text", + "required": true + } + ] + }, + { + "name": "oauth-no-view-with-props", + "title": "OAuth No View with Props", + "subtitle": "Utils Smoke Tests", + "description": "Utils Smoke Tests", + "mode": "no-view", + "arguments": [ + { + "name": "text", + "placeholder": "Text", + "type": "text", + "required": true + } + ] } ], "dependencies": { diff --git a/tests/src/oauth-no-view-with-props.tsx b/tests/src/oauth-no-view-with-props.tsx new file mode 100644 index 0000000..f31e3aa --- /dev/null +++ b/tests/src/oauth-no-view-with-props.tsx @@ -0,0 +1,16 @@ +import { LaunchProps, showHUD } from "@raycast/api"; +import { getAccessToken, withAccessToken, OAuthService } from "@raycast/utils"; + +const linear = OAuthService.linear({ + scope: "read write", + onAuthorize(params) { + console.log(params); + }, +}); + +async function Command(props: LaunchProps<{ arguments: Arguments.OauthNoViewWithProps }>) { + const { token } = getAccessToken(); + await showHUD(`${props.arguments.text} ${token}`); +} + +export default withAccessToken(linear)(Command); diff --git a/tests/src/oauth-with-props.tsx b/tests/src/oauth-with-props.tsx new file mode 100644 index 0000000..c3a98bc --- /dev/null +++ b/tests/src/oauth-with-props.tsx @@ -0,0 +1,12 @@ +import { Detail, LaunchProps } from "@raycast/api"; +import { getAccessToken, withAccessToken, OAuthService } from "@raycast/utils"; + +const github = OAuthService.github({ + scope: "notifications repo read:org read:user read:project", +}); + +function AuthorizedComponent(props: LaunchProps<{ arguments: Arguments.OauthWithProps }>) { + return ; +} + +export default withAccessToken(github)(AuthorizedComponent); From c6c330b5f11ce5dc93e1efae51094b361675c4be Mon Sep 17 00:00:00 2001 From: Thomas Lombart Date: Thu, 1 Feb 2024 16:49:11 +0100 Subject: [PATCH 2/3] OAuth utils: Remove client IDs --- docs/utils-reference/oauth/OAuthService.md | 40 +++++++++++----- docs/utils-reference/oauth/withAccessToken.md | 24 ++++++++++ package.json | 2 +- src/oauth/providers.ts | 48 ++++++++++--------- tests/package-lock.json | 2 +- 5 files changed, 81 insertions(+), 35 deletions(-) diff --git a/docs/utils-reference/oauth/OAuthService.md b/docs/utils-reference/oauth/OAuthService.md index f1c30c7..d2186b0 100644 --- a/docs/utils-reference/oauth/OAuthService.md +++ b/docs/utils-reference/oauth/OAuthService.md @@ -50,7 +50,7 @@ const accessToken = await oauthService.authorize(); ### Built-in Services -Some services are exposed by default to make it easy to authenticate with them. Here's the full list: +We expose by default some services using `OAuthService` to make it easy to authenticate with them: - [Asana](#asana) - [GitHub](#github) @@ -60,13 +60,13 @@ Some services are exposed by default to make it easy to authenticate with them. - [Slack](#slack) - [Zoom](#zoom) -These services are all instances of `OAuthService` with the default options being set. However, you're free to configure your own client ID, and URLs for a specific service. +Some of these services already have a default client configured so that you only have to specify the permission scopes. #### Asana ```tsx const asana = OAuthService.asana({ - clientId: 'custom-client-id', // Optional: If omitted, defaults to a pre-configured client ID + clientId: 'custom-client-id', // Optional: If omitted, defaults to Raycast's client ID for Asana scope: 'default', // Specify the scopes your application requires personalAccessToken: 'personal-access-token', // Optional: For accessing the API directly }); @@ -76,7 +76,7 @@ const asana = OAuthService.asana({ ```tsx const github = OAuthService.github({ - clientId: 'custom-client-id', // Optional: If omitted, defaults to a pre-configured client ID + clientId: 'custom-client-id', // Optional: If omitted, defaults to Raycast's client ID for GitHub scope: 'repo user', // Specify the scopes your application requires personalAccessToken: 'personal-access-token', // Optional: For accessing the API directly }); @@ -84,9 +84,15 @@ const github = OAuthService.github({ #### Google +{% hint style="info" %} +Google has verification processes based on the required scopes for your extension. Therefore, you need to configure your own client for it. +{% endhint %} + ```tsx const google = OAuthService.google({ - clientId: 'custom-client-id', // Optional: If omitted, defaults to a pre-configured client ID + clientId: 'custom-client-id', + authorizeUrl: 'https://accounts.google.com/o/oauth2/v2/auth', + tokenUrl: 'https://oauth2.googleapis.com/token', scope: 'https://www.googleapis.com/auth/drive.readonly', // Specify the scopes your application requires personalAccessToken: 'personal-access-token', // Optional: For accessing the API directly }); @@ -94,10 +100,16 @@ const google = OAuthService.google({ #### Jira +{% hint style="info" %} +Jira requires scopes to be enabled manually in the OAuth app settings. Therefore, you need to configure your own client for it. +{% endhint %} + ```tsx const jira = OAuthService.jira({ - clientId: 'custom-client-id', // Optional: If omitted, defaults to a pre-configured client ID - scope: 'read:jira-user read:jira-work', // Specify the scopes your application requires + clientId: 'custom-client-id', + authorizeUrl: 'https://auth.atlassian.com/authorize', + tokenUrl: 'https://api.atlassian.com/oauth/token', + scope: 'read:jira-user read:jira-work offline_access', // Specify the scopes your application requires personalAccessToken: 'personal-access-token', // Optional: For accessing the API directly }); ``` @@ -106,7 +118,7 @@ const jira = OAuthService.jira({ ```tsx const linear = OAuthService.linear({ - clientId: 'custom-client-id', // Optional: If omitted, defaults to a pre-configured client ID + clientId: 'custom-client-id', // Optional: If omitted, defaults to Raycast's client ID for Linear scope: 'read write', // Specify the scopes your application requires personalAccessToken: 'personal-access-token', // Optional: For accessing the API directly }); @@ -116,7 +128,7 @@ const linear = OAuthService.linear({ ```tsx const slack = OAuthService.slack({ - clientId: 'custom-client-id', // Optional: If omitted, defaults to a pre-configured client ID + clientId: 'custom-client-id', // Optional: If omitted, defaults to Raycast's client ID for Slack scope: 'emoji:read', // Specify the scopes your application requires personalAccessToken: 'personal-access-token', // Optional: For accessing the API directly }); @@ -124,10 +136,16 @@ const slack = OAuthService.slack({ #### Zoom +{% hint style="info" %} +Zoom requires scopes to be enabled manually in the OAuth app settings. Therefore, you need to configure your own client for it. +{% endhint %} + ```tsx const zoom = OAuthService.zoom({ - clientId: 'custom-client-id', // Optional: If omitted, defaults to a pre-configured client ID - scope: '', // Specify the scopes your application requires + clientId: 'custom-client-id', + authorizeUrl: 'https://zoom.us/oauth/authorize', + tokenUrl: 'https://zoom.us/oauth/token', + scope: 'meeting:write', // Specify the scopes your application requires personalAccessToken: 'personal-access-token', // Optional: For accessing the API directly }); ``` diff --git a/docs/utils-reference/oauth/withAccessToken.md b/docs/utils-reference/oauth/withAccessToken.md index 98d39e7..202d98c 100644 --- a/docs/utils-reference/oauth/withAccessToken.md +++ b/docs/utils-reference/oauth/withAccessToken.md @@ -62,6 +62,30 @@ async function AuthorizedCommand() { export default withAccessToken({ authorize })(AuthorizedCommand); ``` +{% endtab %} + +{% tab title="onAuthorize.tsx" %} + +```tsx +import { OAuthService } from "@raycast/utils"; +import { LinearClient, LinearGraphQLClient } from "@linear/sdk"; + +let linearClient: LinearClient | null = null; + +const linear = OAuthService.linear({ + scope: "read write", + onAuthorize({ token }) { + linearClient = new LinearClient({ accessToken: token }); + }, +}); + +function MyIssues() { + return // ... +} + +export default withAccessToken(linear)(View); +``` + {% endtab %} {% endtabs %} diff --git a/package.json b/package.json index ff42c01..079dab8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@raycast/utils", - "version": "1.11.2", + "version": "1.12.0", "description": "Set of utilities to streamline building Raycast extensions", "author": "Raycast Technologies Ltd.", "homepage": "https://developers.raycast.com/utils-reference", diff --git a/src/oauth/providers.ts b/src/oauth/providers.ts index a34f054..6be7aca 100644 --- a/src/oauth/providers.ts +++ b/src/oauth/providers.ts @@ -2,16 +2,23 @@ import { Color, OAuth } from "@raycast/api"; import { OAuthService } from "./OAuthService"; import { OnAuthorizeParams } from "./withAccessToken"; -export type ProviderOptions = { - clientId?: string; +type ClientOptions = { + clientId: string; + authorizeUrl: string; + tokenUrl: string; +}; + +type BaseProviderOptions = { scope: string; - authorizeUrl?: string; - tokenUrl?: string; personalAccessToken?: string; refreshTokenUrl?: string; onAuthorize?: (params: OnAuthorizeParams) => void; }; +export type ProviderWithDefaultClientOptions = BaseProviderOptions & Partial; + +export type ProviderOptions = BaseProviderOptions & ClientOptions; + const PROVIDERS_CONFIG = { asana: { clientId: "1191201745684312", @@ -22,11 +29,9 @@ const PROVIDERS_CONFIG = { icon: ``, }, google: { - clientId: "859594387706-uunbhp90efuesm18epbs0pakuft1m1kt.apps.googleusercontent.com", icon: ``, }, jira: { - clientId: "NAeIO0L9UVdGqKj5YF32HhcysfBCP31P", icon: ``, }, linear: { @@ -38,14 +43,13 @@ const PROVIDERS_CONFIG = { icon: ``, }, zoom: { - clientId: "C_EgncmFQYWrxiZ1lEHFA", icon: ``, }, }; const getIcon = (markup: string) => `data:image/svg+xml,${markup}`; -export const asanaService = (options: ProviderOptions) => +export const asanaService = (options: ProviderWithDefaultClientOptions) => new OAuthService({ client: new OAuth.PKCEClient({ redirectMethod: OAuth.RedirectMethod.Web, @@ -63,7 +67,7 @@ export const asanaService = (options: ProviderOptions) => onAuthorize: options.onAuthorize, }); -export const githubService = (options: ProviderOptions) => +export const githubService = (options: ProviderWithDefaultClientOptions) => new OAuthService({ client: new OAuth.PKCEClient({ redirectMethod: OAuth.RedirectMethod.Web, @@ -90,10 +94,10 @@ export const googleService = (options: ProviderOptions) => providerId: "google", description: "Connect your Google account", }), - clientId: options.clientId ?? PROVIDERS_CONFIG.google.clientId, - authorizeUrl: options.authorizeUrl ?? "https://accounts.google.com/o/oauth2/v2/auth", - tokenUrl: options.tokenUrl ?? "https://oauth2.googleapis.com/token", - refreshTokenUrl: options.tokenUrl ?? "https://oauth2.googleapis.com/token", + clientId: options.clientId, + authorizeUrl: options.authorizeUrl, + tokenUrl: options.tokenUrl, + refreshTokenUrl: options.tokenUrl, scope: options.scope, personalAccessToken: options.personalAccessToken, bodyEncoding: "url-encoded", @@ -109,16 +113,16 @@ export const jiraService = (options: ProviderOptions) => providerId: "jira", description: "Connect your Jira account", }), - clientId: options.clientId ?? PROVIDERS_CONFIG.jira.clientId, - authorizeUrl: options.authorizeUrl ?? "https://jira.oauth.raycast.com/authorize", - tokenUrl: options.tokenUrl ?? "https://jira.oauth.raycast.com/token", - refreshTokenUrl: options.refreshTokenUrl ?? "https://jira.oauth.raycast.com/refresh-token", + clientId: options.clientId, + authorizeUrl: options.authorizeUrl, + tokenUrl: options.tokenUrl, + refreshTokenUrl: options.refreshTokenUrl, scope: options.scope, personalAccessToken: options.personalAccessToken, onAuthorize: options.onAuthorize, }); -export const linearService = (options: ProviderOptions) => +export const linearService = (options: ProviderWithDefaultClientOptions) => new OAuthService({ client: new OAuth.PKCEClient({ redirectMethod: OAuth.RedirectMethod.Web, @@ -167,10 +171,10 @@ export const zoomService = (options: ProviderOptions) => providerId: "zoom", description: "Connect your Zoom account", }), - clientId: options.clientId ?? PROVIDERS_CONFIG.zoom.clientId, - authorizeUrl: options.authorizeUrl ?? "https://zoom.oauth.raycast.com/authorize", - tokenUrl: options.tokenUrl ?? "https://zoom.oauth.raycast.com/token", - refreshTokenUrl: options.refreshTokenUrl ?? "https://zoom.oauth.raycast.com/refresh-token", + clientId: options.clientId, + authorizeUrl: options.authorizeUrl, + tokenUrl: options.tokenUrl, + refreshTokenUrl: options.refreshTokenUrl, scope: options.scope, personalAccessToken: options.personalAccessToken, bodyEncoding: "url-encoded", diff --git a/tests/package-lock.json b/tests/package-lock.json index 16797df..c3446d9 100644 --- a/tests/package-lock.json +++ b/tests/package-lock.json @@ -22,7 +22,7 @@ }, "..": { "name": "@raycast/utils", - "version": "1.11.1", + "version": "1.12.0", "license": "MIT", "dependencies": { "content-type": "^1.0.5", From 4e4b3172ca8ad90ba45e9b28463da835621cf692 Mon Sep 17 00:00:00 2001 From: Thomas Lombart Date: Thu, 1 Feb 2024 17:07:30 +0100 Subject: [PATCH 3/3] Update withAccessToken signature --- docs/utils-reference/oauth/withAccessToken.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/utils-reference/oauth/withAccessToken.md b/docs/utils-reference/oauth/withAccessToken.md index 202d98c..2e5d888 100644 --- a/docs/utils-reference/oauth/withAccessToken.md +++ b/docs/utils-reference/oauth/withAccessToken.md @@ -5,11 +5,11 @@ Higher-order function fetching an authorization token to then access it. This ma ## Signature ```tsx -function withAccessToken( +function withAccessToken( options: WithAccessTokenParameters, -): Promise | void) | React.ComponentType>( +): >( fnOrComponent: U, -) => U extends () => Promise | void ? Promise : React.FunctionComponent; +) => U extends (props: T) => Promise | void ? Promise : React.FunctionComponent; ``` ### Arguments