Skip to content

Commit 5b641de

Browse files
authored
fix: error handling for oauth errors (#39)
- Adds `picker:oauth:error`and `picker:oauth:response` events - Removed `picker:authenticated` event from the documentation
1 parent e2668df commit 5b641de

File tree

4 files changed

+94
-36
lines changed

4 files changed

+94
-36
lines changed

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,12 +199,13 @@ by using the component attributes mapped to the corresponding methods of
199199

200200
#### Events
201201

202-
| Name | Type | Description |
203-
| ---------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
204-
| `picker:authenticated` | `{ token: string }` | Triggered when the user authenticates with the provided OAuth client ID and scope. |
205-
| `picker:canceled` | `google.picker.ResponseObject` | Triggered when the user cancels the picker dialog. See [`ResponseObject`](https://developers.google.com/drive/picker/reference/picker.responseobject). |
206-
| `picker:picked` | `google.picker.ResponseObject` | Triggered when the user picks one or more items. See [`ResponseObject`](https://developers.google.com/drive/picker/reference/picker.responseobject). |
207-
| `picker:error` | `google.picker.ResponseObject` | Triggered when an error occurs. See [`ResponseObject`](https://developers.google.com/drive/picker/reference/picker.responseobject). |
202+
| Name | Type | Description |
203+
| ----------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
204+
| `picker:oauth:error` | `google.accounts.oauth2.ClientConfigError` | Triggered when an error occurs in the OAuth flow. See the [error guide](https://developers.google.com/identity/oauth2/web/guides/error). |
205+
| `picker:oauth:response` | `google.accounts.oauth2.ClientConfigError` | Triggered when an OAuth flow completes. See the [token model guide](https://developers.google.com/identity/oauth2/web/guides/use-token-model). |
206+
| `picker:canceled` | `google.picker.ResponseObject` | Triggered when the user cancels the picker dialog. See [`ResponseObject`](https://developers.google.com/drive/picker/reference/picker.responseobject). |
207+
| `picker:picked` | `google.picker.ResponseObject` | Triggered when the user picks one or more items. See [`ResponseObject`](https://developers.google.com/drive/picker/reference/picker.responseobject). |
208+
| `picker:error` | `google.picker.ResponseObject` | Triggered when an error occurs. See [`ResponseObject`](https://developers.google.com/drive/picker/reference/picker.responseobject). |
208209

209210
#### Slots
210211

custom-elements.json

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,22 +248,45 @@
248248
}
249249
}
250250
]
251+
},
252+
{
253+
"kind": "method",
254+
"name": "retrieveAccessToken",
255+
"privacy": "private",
256+
"return": {
257+
"type": {
258+
"text": "Promise<string | undefined>"
259+
}
260+
}
251261
}
252262
],
253263
"events": [
254264
{
255-
"name": "picker:authenticated",
265+
"name": "eventType",
256266
"type": {
257-
"text": "{ token: string }"
267+
"text": "CustomEvent"
268+
}
269+
},
270+
{
271+
"name": "picker:oauth:error",
272+
"type": {
273+
"text": "google.accounts.oauth2.ClientConfigError"
258274
},
259-
"description": "Triggered when the user authenticates with the provided OAuth client ID and scope."
275+
"description": "Triggered when an error occurs in the OAuth flow. See the [error guide](https://developers.google.com/identity/oauth2/web/guides/error)."
260276
},
261277
{
262-
"name": "eventType",
278+
"name": "picker:authenticated",
263279
"type": {
264280
"text": "CustomEvent"
265281
}
266282
},
283+
{
284+
"name": "picker:oauth:response",
285+
"type": {
286+
"text": "google.accounts.oauth2.ClientConfigError"
287+
},
288+
"description": "Triggered when an OAuth flow completes. See the [token model guide](https://developers.google.com/identity/oauth2/web/guides/use-token-model)."
289+
},
267290
{
268291
"type": {
269292
"text": "google.picker.ResponseObject"

src/drive-picker/drive-picker-element.ts

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ interface DrivePickerDocsViewElement extends HTMLElement {
3030

3131
declare global {
3232
interface GlobalEventHandlersEventMap {
33+
/** @deprecated - Use "picker:oauth:response" */
3334
"picker:authenticated": CustomEvent<{ token: string }>;
35+
"picker:oauth:error": CustomEvent<
36+
| google.accounts.oauth2.ClientConfigError
37+
| google.accounts.oauth2.TokenResponse
38+
>;
39+
"picker:oauth:response": CustomEvent<google.accounts.oauth2.TokenResponse>;
3440
"picker:canceled": CustomEvent<google.picker.ResponseObject>;
3541
"picker:picked": CustomEvent<google.picker.ResponseObject>;
3642
"picker:error": CustomEvent<unknown>;
@@ -46,11 +52,11 @@ declare global {
4652
*
4753
* @element drive-picker
4854
*
49-
* @fires {{ token: string }} picker:authenticated - Triggered when the user authenticates with the
50-
* provided OAuth client ID and scope.
5155
* @fires {google.picker.ResponseObject} picker:canceled - Triggered when the user cancels the picker dialog. See [`ResponseObject`](https://developers.google.com/drive/picker/reference/picker.responseobject).
5256
* @fires {google.picker.ResponseObject} picker:picked - Triggered when the user picks one or more items. See [`ResponseObject`](https://developers.google.com/drive/picker/reference/picker.responseobject).
5357
* @fires {google.picker.ResponseObject} picker:error - Triggered when an error occurs. See [`ResponseObject`](https://developers.google.com/drive/picker/reference/picker.responseobject).
58+
* @fires {google.accounts.oauth2.ClientConfigError} picker:oauth:error - Triggered when an error occurs in the OAuth flow. See the [error guide](https://developers.google.com/identity/oauth2/web/guides/error).
59+
* @fires {google.accounts.oauth2.ClientConfigError} picker:oauth:response - Triggered when an OAuth flow completes. See the [token model guide](https://developers.google.com/identity/oauth2/web/guides/use-token-model).
5460
*
5561
* @slot - The default slot contains View elements to display in the picker.
5662
* Each View element should implement a property `view` of type
@@ -175,18 +181,9 @@ export class DrivePickerElement extends HTMLElement {
175181

176182
// OAuth token is required either as an attribute or from the OAuth flow using the client ID and scope
177183
const oauthToken =
178-
this.getAttribute("oauth-token") ??
179-
(await retrieveAccessToken(
180-
// biome-ignore lint/style/noNonNullAssertion: just let the error bubble up when null
181-
this.getAttribute("client-id")!,
182-
this.getAttribute("scope") ??
183-
"https://www.googleapis.com/auth/drive.file",
184-
).then((token) => {
185-
this.dispatchEvent(
186-
new CustomEvent("picker:authenticated", { detail: { token } }),
187-
);
188-
return token;
189-
}));
184+
this.getAttribute("oauth-token") ?? (await this.retrieveAccessToken());
185+
186+
if (!oauthToken) return;
190187

191188
// biome-ignore lint/style/noNonNullAssertion: <explanation>
192189
builder = builder.setOAuthToken(oauthToken!);
@@ -272,6 +269,42 @@ export class DrivePickerElement extends HTMLElement {
272269
);
273270
}
274271

272+
private retrieveAccessToken(): Promise<string | undefined> {
273+
return retrieveAccessToken(
274+
// biome-ignore lint/style/noNonNullAssertion: just let the error bubble up when null
275+
this.getAttribute("client-id")!,
276+
this.getAttribute("scope") ??
277+
"https://www.googleapis.com/auth/drive.file",
278+
)
279+
.then((response) => {
280+
const { access_token: token } = response;
281+
if (!token) {
282+
this.dispatchEvent(
283+
new CustomEvent("picker:oauth:error", {
284+
detail: response,
285+
}),
286+
);
287+
return undefined;
288+
}
289+
// TODO - remove deprecated event
290+
this.dispatchEvent(
291+
new CustomEvent("picker:authenticated", { detail: { token } }),
292+
);
293+
this.dispatchEvent(
294+
new CustomEvent("picker:oauth:response", { detail: response }),
295+
);
296+
return token;
297+
})
298+
.catch((error) => {
299+
this.dispatchEvent(
300+
new CustomEvent("picker:oauth:error", {
301+
detail: error,
302+
}),
303+
);
304+
return undefined;
305+
});
306+
}
307+
275308
disconnectedCallback(): void {
276309
this.picker?.dispose();
277310
}

src/utils.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
const GAPI_URL = "https://apis.google.com/js/api.js";
18-
const GSI_URL = "https://accounts.google.com/gsi/client";
18+
export const GSI_URL = "https://accounts.google.com/gsi/client";
1919

2020
export async function loadApi(api = "client:picker"): Promise<typeof google> {
2121
if (!window.gapi) {
@@ -29,24 +29,27 @@ export async function loadApi(api = "client:picker"): Promise<typeof google> {
2929
return window.google;
3030
}
3131

32+
export class ClientConfigError extends Error {
33+
constructor(
34+
public readonly configError: google.accounts.oauth2.ClientConfigError,
35+
) {
36+
super(configError.message);
37+
}
38+
}
39+
3240
export async function retrieveAccessToken(
3341
clientId: string,
3442
scope: string,
35-
): Promise<string> {
43+
): Promise<google.accounts.oauth2.TokenResponse> {
3644
if (!window.google?.accounts?.oauth2) {
3745
await injectScript(GSI_URL);
3846
}
3947
return new Promise((resolve, reject) => {
4048
const client = window.google.accounts.oauth2.initTokenClient({
4149
client_id: clientId,
4250
scope: scope.replace(/,/g, " ").replace(/\s+/g, " "),
43-
44-
callback: ({ access_token }) => {
45-
resolve(access_token);
46-
},
47-
error_callback: (error) => {
48-
reject(error);
49-
},
51+
callback: resolve,
52+
error_callback: reject,
5053
});
5154

5255
client.requestAccessToken();
@@ -60,9 +63,7 @@ export async function injectScript(src: string): Promise<void> {
6063
Object.assign(document.createElement("script"), {
6164
src,
6265
onload: resolve,
63-
onerror: () => {
64-
reject(`error loading ${src}`);
65-
},
66+
onerror: reject,
6667
}),
6768
);
6869
} else {

0 commit comments

Comments
 (0)