From bbb339d86f016f685f1268edb722351c4bbf1f5d Mon Sep 17 00:00:00 2001 From: damencho Date: Fri, 3 Mar 2023 12:51:38 -0600 Subject: [PATCH] fix: Updates gapi to use new google identity service. fix: Updates gapi to use new google identity service. --- react/features/google-api/actions.js | 10 +- react/features/google-api/constants.ts | 12 +- react/features/google-api/googleApi.web.js | 160 +++++++++++++++------ react/features/google-api/logger.ts | 3 + 4 files changed, 132 insertions(+), 53 deletions(-) create mode 100644 react/features/google-api/logger.ts diff --git a/react/features/google-api/actions.js b/react/features/google-api/actions.js index 8114e3fd5..67592ad91 100644 --- a/react/features/google-api/actions.js +++ b/react/features/google-api/actions.js @@ -52,6 +52,7 @@ export function loadGoogleAPI() { return Promise.resolve(); }) .then(() => dispatch(setGoogleAPIState(GOOGLE_API_STATES.LOADED))) + .then(() => googleApi.signInIfNotSignedIn()) .then(() => googleApi.isSignedIn()) .then(isSignedIn => { if (isSignedIn) { @@ -150,8 +151,7 @@ export function setGoogleAPIState( * selectedBoundStreamID: *} | never>)} */ export function showAccountSelection() { - return () => - googleApi.showAccountSelection(); + return () => googleApi.showAccountSelection(true); } /** @@ -161,7 +161,7 @@ export function showAccountSelection() { */ export function signIn() { return (dispatch: Dispatch) => googleApi.get() - .then(() => googleApi.signInIfNotSignedIn()) + .then(() => googleApi.signInIfNotSignedIn(true)) .then(() => dispatch({ type: SET_GOOGLE_API_STATE, googleAPIState: GOOGLE_API_STATES.SIGNED_IN @@ -205,10 +205,10 @@ export function updateProfile() { .then(profile => { dispatch({ type: SET_GOOGLE_API_PROFILE, - profileEmail: profile.getEmail() + profileEmail: profile.email }); - return profile.getEmail(); + return profile.email; }); } diff --git a/react/features/google-api/constants.ts b/react/features/google-api/constants.ts index 6c6af5a01..efe3d6f73 100644 --- a/react/features/google-api/constants.ts +++ b/react/features/google-api/constants.ts @@ -20,10 +20,9 @@ export const API_URL_LIVE_BROADCASTS = 'https://content.googleapis.com/youtube/v /** * Array of API discovery doc URLs for APIs used by the googleApi. * - * @type {string[]} + * @type {string} */ -export const DISCOVERY_DOCS - = [ 'https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest' ]; +export const DISCOVERY_DOCS = 'https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest'; /** * An enumeration of the different states the Google API can be in. @@ -61,6 +60,13 @@ export const GOOGLE_API_STATES = { */ export const GOOGLE_SCOPE_CALENDAR = 'https://www.googleapis.com/auth/calendar'; +/** + * Google API auth scope to access user email. + * + * @type {string} + */ +export const GOOGLE_SCOPE_USERINFO = 'https://www.googleapis.com/auth/userinfo.email'; + /** * Google API auth scope to access YouTube streams. * diff --git a/react/features/google-api/googleApi.web.js b/react/features/google-api/googleApi.web.js index f3f147075..7bbe2ef1c 100644 --- a/react/features/google-api/googleApi.web.js +++ b/react/features/google-api/googleApi.web.js @@ -3,10 +3,13 @@ import { API_URL_LIVE_BROADCASTS, DISCOVERY_DOCS, GOOGLE_SCOPE_CALENDAR, + GOOGLE_SCOPE_USERINFO, GOOGLE_SCOPE_YOUTUBE } from './constants'; +import logger from './logger'; const GOOGLE_API_CLIENT_LIBRARY_URL = 'https://apis.google.com/js/api.js'; +const GOOGLE_GIS_LIBRARY_URL = 'https://accounts.google.com/gsi/client'; /** * A promise for dynamically loading the Google API Client Library. @@ -50,9 +53,9 @@ const googleApi = { } return this._getGoogleApiClient() - .auth2.getAuthInstance() - .currentUser.get() - .getBasicProfile(); + .client.oauth2 + .userinfo.get().getPromise() + .then(r => r.result); }); }, @@ -68,23 +71,40 @@ const googleApi = { initializeClient(clientId, enableYoutube, enableCalendar) { return this.get() .then(api => new Promise((resolve, reject) => { - const scope - = `${enableYoutube ? GOOGLE_SCOPE_YOUTUBE : ''} ${enableCalendar ? GOOGLE_SCOPE_CALENDAR : ''}` - .trim(); - // setTimeout is used as a workaround for api.client.init not // resolving consistently when the Google API Client Library is // loaded asynchronously. See: // github.com/google/google-api-javascript-client/issues/399 setTimeout(() => { - api.client.init({ - clientId, - discoveryDocs: DISCOVERY_DOCS, - scope + api.client.init({}) + .then(() => { + if (enableCalendar) { + api.client.load(DISCOVERY_DOCS); + } + }) + .then(() => { + api.client.load('https://www.googleapis.com/discovery/v1/apis/oauth2/v1/rest'); }) .then(resolve) .catch(reject); }, 500); + })) + .then(() => new Promise((resolve, reject) => { + try { + const scope + = `${enableYoutube ? GOOGLE_SCOPE_YOUTUBE : ''} ${enableCalendar ? GOOGLE_SCOPE_CALENDAR : ''}` + .trim(); + + this.tokenClient = this._getGoogleGISApiClient().accounts.oauth2.initTokenClient({ + // eslint-disable-next-line camelcase + client_id: clientId, + scope: `${scope} ${GOOGLE_SCOPE_USERINFO}`, + callback: '' // defined at request time in await/promise scope. + }); + resolve(); + } catch (err) { + reject(err); + } })); }, @@ -95,13 +115,38 @@ const googleApi = { * @returns {Promise} */ isSignedIn() { - return this.get() - .then(api => Boolean(api - && api.auth2 - && api.auth2.getAuthInstance - && api.auth2.getAuthInstance() - && api.auth2.getAuthInstance().isSignedIn - && api.auth2.getAuthInstance().isSignedIn.get())); + return new Promise((resolve, _) => { + const te = parseInt(this.tokenExpires, 10); + const isExpired = isNaN(this.tokenExpires) ? true : new Date().getTime() > te; + + resolve(Boolean(!isExpired)); + }); + }, + + /** + * Generates a script tag. + * + * @param {string} src - The source for the script tag. + * @returns {Promise} + * @private + */ + _loadScriptTag(src) { + return new Promise((resolve, reject) => { + const scriptTag = document.createElement('script'); + + scriptTag.async = true; + scriptTag.addEventListener('error', () => { + scriptTag.remove(); + + reject(); + }); + scriptTag.addEventListener('load', resolve); + scriptTag.type = 'text/javascript'; + + scriptTag.src = src; + + document.head.appendChild(scriptTag); + }); }, /** @@ -114,29 +159,19 @@ const googleApi = { return googleClientLoadPromise; } - googleClientLoadPromise = new Promise((resolve, reject) => { - const scriptTag = document.createElement('script'); - - scriptTag.async = true; - scriptTag.addEventListener('error', () => { - scriptTag.remove(); - + googleClientLoadPromise = this._loadScriptTag(GOOGLE_API_CLIENT_LIBRARY_URL) + .catch(() => { googleClientLoadPromise = null; - - reject(); - }); - scriptTag.addEventListener('load', resolve); - scriptTag.type = 'text/javascript'; - - scriptTag.src = GOOGLE_API_CLIENT_LIBRARY_URL; - - document.head.appendChild(scriptTag); - }) + }) .then(() => new Promise((resolve, reject) => - this._getGoogleApiClient().load('client:auth2', { + this._getGoogleApiClient().load('client', { callback: resolve, onerror: reject }))) + .then(this._loadScriptTag(GOOGLE_GIS_LIBRARY_URL)) + .catch(() => { + googleClientLoadPromise = null; + }) .then(() => this._getGoogleApiClient()); return googleClientLoadPromise; @@ -171,25 +206,50 @@ const googleApi = { * Prompts the participant to sign in to the Google API Client Library, even * if already signed in. * + * @param {boolean} consent - Whether to show account selection dialog. * @returns {Promise} */ - showAccountSelection() { + showAccountSelection(consent: boolean) { return this.get() - .then(api => api.auth2.getAuthInstance().signIn()); + .then(api => new Promise((resolve, reject) => { + try { + // Settle this promise in the response callback for requestAccessToken() + this.tokenClient.callback = resp => { + if (resp.error !== undefined) { + reject(resp); + } + + // Get the number of seconds the token is valid for, subtract 5 minutes + // to account for differences in clock settings and convert to ms. + const expiresIn = (parseInt(api.client.getToken().expires_in, 10) - 300) * 1000; + const now = new Date(); + const expireDate = new Date(now.getTime() + expiresIn); + + this.tokenExpires = expireDate.getTime().toString(); + + resolve(resp); + }; + + this.tokenClient.requestAccessToken({ prompt: consent ? 'consent' : '' }); + } catch (err) { + logger.error('Error requesting token', err); + } + })); }, /** * Prompts the participant to sign in to the Google API Client Library, if * not already signed in. * + * @param {boolean} consent - Whether to show account selection dialog. * @returns {Promise} */ - signInIfNotSignedIn() { + signInIfNotSignedIn(consent: boolean) { return this.get() .then(() => this.isSignedIn()) .then(isSignedIn => { if (!isSignedIn) { - return this.showAccountSelection(); + return this.showAccountSelection(consent); } }); }, @@ -201,11 +261,10 @@ const googleApi = { */ signOut() { return this.get() - .then(api => - api.auth2 - && api.auth2.getAuthInstance - && api.auth2.getAuthInstance() - && api.auth2.getAuthInstance().signOut()); + .then(() => { + this.tokenClient = undefined; + this.tokenExpires = undefined; + }); }, /** @@ -387,6 +446,17 @@ const googleApi = { */ _getGoogleApiClient() { return window.gapi; + }, + + + /** + * Returns the global Google Identity Services Library object. + * + * @private + * @returns {Object|undefined} + */ + _getGoogleGISApiClient() { + return window.google; } }; diff --git a/react/features/google-api/logger.ts b/react/features/google-api/logger.ts new file mode 100644 index 000000000..0ed8e1853 --- /dev/null +++ b/react/features/google-api/logger.ts @@ -0,0 +1,3 @@ +import { getLogger } from '../base/logging/functions'; + +export default getLogger('features/base/redux');