Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OAuth utils: Remove some clients and fix no-view props #22

Merged
merged 3 commits into from
Feb 1, 2024
Merged
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
40 changes: 29 additions & 11 deletions docs/utils-reference/oauth/OAuthService.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
});
Expand All @@ -76,28 +76,40 @@ 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
});
```

#### 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
});
```

#### 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
});
```
Expand All @@ -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
});
Expand All @@ -116,18 +128,24 @@ 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
});
```

#### 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
});
```
Expand Down
30 changes: 27 additions & 3 deletions docs/utils-reference/oauth/withAccessToken.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ Higher-order function fetching an authorization token to then access it. This ma
## Signature

```tsx
function withAccessToken<T>(
function withAccessToken<T = any>(
options: WithAccessTokenParameters,
): <U extends (() => Promise<void> | void) | React.ComponentType<T>>(
): <U extends WithAccessTokenComponentOrFn<T>>(
fnOrComponent: U,
) => U extends () => Promise<void> | void ? Promise<void> : React.FunctionComponent<T>;
) => U extends (props: T) => Promise<void> | void ? Promise<void> : React.FunctionComponent<T>;
```

### Arguments
Expand Down Expand Up @@ -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 %}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@raycast/utils",
"version": "1.11.1",
"version": "1.12.0",
"description": "Set of utilities to streamline building Raycast extensions",
"author": "Raycast Technologies Ltd.",
"homepage": "https://developers.raycast.com/utils-reference",
Expand Down
48 changes: 26 additions & 22 deletions src/oauth/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClientOptions>;

export type ProviderOptions = BaseProviderOptions & ClientOptions;

const PROVIDERS_CONFIG = {
asana: {
clientId: "1191201745684312",
Expand All @@ -22,11 +29,9 @@ const PROVIDERS_CONFIG = {
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>`,
},
google: {
clientId: "859594387706-uunbhp90efuesm18epbs0pakuft1m1kt.apps.googleusercontent.com",
icon: `<svg xmlns="http://www.w3.org/2000/svg" style="display:block" viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/><path fill="none" d="M0 0h48v48H0z"/></svg>`,
},
jira: {
clientId: "NAeIO0L9UVdGqKj5YF32HhcysfBCP31P",
icon: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2361" height="2500" viewBox="2.59 0 214.091 224"><linearGradient id="a" x1="102.4" x2="56.15" y1="218.63" y2="172.39" gradientTransform="matrix(1 0 0 -1 0 264)" gradientUnits="userSpaceOnUse"><stop offset=".18" stop-color="#0052cc"/><stop offset="1" stop-color="#2684ff"/></linearGradient><linearGradient xlink:href="#a" id="b" x1="114.65" x2="160.81" y1="85.77" y2="131.92"/><path fill="#2684ff" d="M214.06 105.73 117.67 9.34 108.33 0 35.77 72.56 2.59 105.73a8.89 8.89 0 0 0 0 12.54l66.29 66.29L108.33 224l72.55-72.56 1.13-1.12 32.05-32a8.87 8.87 0 0 0 0-12.59zm-105.73 39.39L75.21 112l33.12-33.12L141.44 112z"/><path fill="url(#a)" d="M108.33 78.88a55.75 55.75 0 0 1-.24-78.61L35.62 72.71l39.44 39.44z"/><path fill="url(#b)" d="m141.53 111.91-33.2 33.21a55.77 55.77 0 0 1 0 78.86L181 151.35z"/></svg>`,
},
linear: {
Expand All @@ -38,14 +43,13 @@ const PROVIDERS_CONFIG = {
icon: `<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="Layer_1" x="0" y="0" style="enable-background:new 0 0 270 270" version="1.1" viewBox="0 0 270 270"><style>.st0{fill:#e01e5a}.st1{fill:#36c5f0}.st2{fill:#2eb67d}.st3{fill:#ecb22e}</style><path d="M99.4 151.2c0 7.1-5.8 12.9-12.9 12.9-7.1 0-12.9-5.8-12.9-12.9 0-7.1 5.8-12.9 12.9-12.9h12.9v12.9zM105.9 151.2c0-7.1 5.8-12.9 12.9-12.9s12.9 5.8 12.9 12.9v32.3c0 7.1-5.8 12.9-12.9 12.9s-12.9-5.8-12.9-12.9v-32.3z" class="st0"/><path d="M118.8 99.4c-7.1 0-12.9-5.8-12.9-12.9 0-7.1 5.8-12.9 12.9-12.9s12.9 5.8 12.9 12.9v12.9h-12.9zM118.8 105.9c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9H86.5c-7.1 0-12.9-5.8-12.9-12.9s5.8-12.9 12.9-12.9h32.3z" class="st1"/><path d="M170.6 118.8c0-7.1 5.8-12.9 12.9-12.9 7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9h-12.9v-12.9zM164.1 118.8c0 7.1-5.8 12.9-12.9 12.9-7.1 0-12.9-5.8-12.9-12.9V86.5c0-7.1 5.8-12.9 12.9-12.9 7.1 0 12.9 5.8 12.9 12.9v32.3z" class="st2"/><path d="M151.2 170.6c7.1 0 12.9 5.8 12.9 12.9 0 7.1-5.8 12.9-12.9 12.9-7.1 0-12.9-5.8-12.9-12.9v-12.9h12.9zM151.2 164.1c-7.1 0-12.9-5.8-12.9-12.9 0-7.1 5.8-12.9 12.9-12.9h32.3c7.1 0 12.9 5.8 12.9 12.9 0 7.1-5.8 12.9-12.9 12.9h-32.3z" class="st3"/></svg>`,
},
zoom: {
clientId: "C_EgncmFQYWrxiZ1lEHFA",
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 351.845 80"><path d="M73.786 78.835H10.88A10.842 10.842 0 0 1 .833 72.122a10.841 10.841 0 0 1 2.357-11.85L46.764 16.7h-31.23C6.954 16.699 0 9.744 0 1.165h58.014c4.414 0 8.357 2.634 10.046 6.712a10.843 10.843 0 0 1-2.356 11.85L22.13 63.302h36.122c8.58 0 15.534 6.955 15.534 15.534Zm278.059-48.544C351.845 13.588 338.256 0 321.553 0c-8.934 0-16.975 3.89-22.524 10.063C293.48 3.89 285.44 0 276.505 0c-16.703 0-30.291 13.588-30.291 30.291v48.544c8.579 0 15.534-6.955 15.534-15.534v-33.01c0-8.137 6.62-14.757 14.757-14.757s14.757 6.62 14.757 14.757v33.01c0 8.58 6.955 15.534 15.534 15.534V30.291c0-8.137 6.62-14.757 14.757-14.757s14.758 6.62 14.758 14.757v33.01c0 8.58 6.954 15.534 15.534 15.534V30.291ZM238.447 40c0 22.091-17.909 40-40 40s-40-17.909-40-40 17.908-40 40-40 40 17.909 40 40Zm-15.534 0c0-13.512-10.954-24.466-24.466-24.466S173.98 26.488 173.98 40s10.953 24.466 24.466 24.466S222.913 53.512 222.913 40Zm-70.68 0c0 22.091-17.909 40-40 40s-40-17.909-40-40 17.909-40 40-40 40 17.909 40 40Zm-15.534 0c0-13.512-10.954-24.466-24.466-24.466S87.767 26.488 87.767 40s10.954 24.466 24.466 24.466S136.699 53.512 136.699 40Z" style="fill:#0b5cff"/></svg>`,
},
};

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,
Expand All @@ -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,
Expand All @@ -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",
Expand All @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
33 changes: 20 additions & 13 deletions src/oauth/withAccessToken.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
onAuthorize?: (params: OnAuthorizeParams) => void;
};

export type WithAccessTokenComponentOrFn<T = any> = ((params: T) => Promise<void> | void) | React.ComponentType<T>;

Check warning on line 43 in src/oauth/withAccessToken.tsx

View workflow job for this annotation

GitHub Actions / tests

Unexpected any. Specify a different type

/**
* Higher-order component to wrap a given component or function and set an access token in a shared global variable.
*
Expand Down Expand Up @@ -67,24 +69,29 @@
* @param {string} options.personalAccessToken - An optional personal access token.
* @returns {React.ComponentType<T>} The wrapped component.
*/
export function withAccessToken<T>(
export function withAccessToken<T = any>(

Check warning on line 72 in src/oauth/withAccessToken.tsx

View workflow job for this annotation

GitHub Actions / tests

Unexpected any. Specify a different type
options: WithAccessTokenParameters,
): <U extends (() => Promise<void> | void) | React.ComponentType<T>>(
): <U extends WithAccessTokenComponentOrFn<T>>(
fnOrComponent: U,
) => U extends () => Promise<void> | void ? Promise<void> : React.FunctionComponent<T>;
) => U extends (props: T) => Promise<void> | void ? Promise<void> : React.FunctionComponent<T>;
export function withAccessToken<T>(options: WithAccessTokenParameters) {
if (environment.commandMode === "no-view") {
return async (fn: () => Promise<void> | (() => 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> | (() => 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;
};
}

Expand Down
2 changes: 1 addition & 1 deletion tests/package-lock.json

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

40 changes: 39 additions & 1 deletion tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading
Loading