diff --git a/gatsby/onInitialClientRender.js b/gatsby/onInitialClientRender.js index 732687e7b..781b8e9db 100644 --- a/gatsby/onInitialClientRender.js +++ b/gatsby/onInitialClientRender.js @@ -2,61 +2,98 @@ import loadScript from "load-script" import { store } from "./wrapRootElement" export const onInitialClientRender = () => { - loadScript(`https://apis.google.com/js/api.js`, err => { - if (err) { - console.error("Could not load gapi") - return - } + const gapiPromise = new Promise((resolve, reject) => { + loadScript("https://apis.google.com/js/api.js", err => { + if (err) { + reject(err) + } else { + window.gapi.load("client", () => { + resolve(window.gapi) + }) + } + }) + }) + + const gisPromise = new Promise((resolve, reject) => { + loadScript("https://accounts.google.com/gsi/client", err => { + if (err) { + reject(err) + } else resolve(window.google) + }) + }) + + Promise.all([gapiPromise, gisPromise]) + .then(([gapi, google]) => { + const SCOPES = ["https://www.googleapis.com/auth/analytics.readonly"] + const clientId = process.env.GAPI_CLIENT_ID - var SCOPES = ["https://www.googleapis.com/auth/analytics.readonly"] + if (!clientId) { + console.error( + "GAPI_CLIENT_ID is not defined. Please check your .env file." + ) + store.dispatch({ type: "gapiStatus", status: "cannot initialize" }) + return + } - const clientId = process.env.GAPI_CLIENT_ID + // Google Sign-In previously helped manage user signed-in status + // With GIS we are responsible for managing sign-in state + try { + const storedTokenString = localStorage.getItem("google_token") + if (storedTokenString) { + const storedToken = JSON.parse(storedTokenString) + if (storedToken.expires_at > Date.now()) { + gapi.client.setToken(storedToken) + store.dispatch({ type: "setToken", token: storedToken }) + } else { + localStorage.removeItem("google_token") + } + } + } catch (e) { + console.error("Unable to restore token from localStorage:", e) + localStorage.removeItem("google_token") + } - // TODO - Remove :analytics and replace it with the discovery document. - window.gapi.load("client:auth2:analytics", () => { Promise.all([ - window.gapi.client.load( - "https://analyticsreporting.googleapis.com/$discovery/rest?version=v4" - ), - window.gapi.client.load( + gapi.client.load( "https://analyticsdata.googleapis.com/$discovery/rest" ), - window.gapi.client.load( + gapi.client.load( "https://analyticsadmin.googleapis.com/$discovery/rest" ), - ]).then(() => { - window.gapi.client - .init({ + ]) + .then(() => { + // Replace gapi.auth2.init() with google.accounts.oauth2.initTokenClient() + const tokenClient = google.accounts.oauth2.initTokenClient({ + client_id: clientId, scope: SCOPES.join(" "), - clientId, - }) - .then(() => { - store.dispatch({ type: "setGapi", gapi: window.gapi }) - const user = window.gapi.auth2.getAuthInstance().currentUser.get() - store.dispatch({ - type: "setUser", - user: user.isSignedIn() ? user : undefined, - }) - window.gapi.auth2.getAuthInstance().currentUser.listen(user => { - store.dispatch({ - type: "setUser", - user: user.isSignedIn() ? user : undefined, - }) - }) - }) - .catch(e => { - store.dispatch({ type: "setGapi", gapi: window.gapi }) - store.dispatch({ - type: "setUser", - user: undefined, - }) - store.dispatch({ - type: "gapiStatus", - status: "cannot initialize", - }) - console.error(e) + callback: tokenResponse => { + if (tokenResponse && tokenResponse.access_token) { + const tokenWithExpiry = { + ...tokenResponse, + expires_at: Date.now() + tokenResponse.expires_in * 1000, + } + localStorage.setItem("google_token", JSON.stringify(tokenWithExpiry)) + gapi.client.setToken(tokenResponse) + store.dispatch({ type: "setToken", token: tokenResponse }) + } else { + store.dispatch({ type: "setToken", token: undefined }) + } + } }) - }, console.error) + + store.dispatch({ type: "setGapi", gapi }) + store.dispatch({ type: "gapiStatus", status: "initialized" }) + store.dispatch({ type: "setGoogle", google }) + store.dispatch({ type: "setTokenClient", tokenClient }) + }) + .catch(e => { + store.dispatch({ type: "setGapi", gapi }) + store.dispatch({ type: "setToken", token: undefined }) + store.dispatch({ type: "gapiStatus", status: "cannot initialize" }) + console.error(e) + }) + }) + .catch(e => { + console.error(e) }) - }) } diff --git a/gatsby/wrapRootElement.tsx b/gatsby/wrapRootElement.tsx index a1d347276..970898d6a 100644 --- a/gatsby/wrapRootElement.tsx +++ b/gatsby/wrapRootElement.tsx @@ -18,23 +18,31 @@ import {PartialDeep} from 'type-fest'; type State = { - user?: {}, + token?: {}, gapi?: PartialDeep, + google?: any, + tokenClient?: any, toast?: string, - status?: string + gapiStatus?: string } type Action = - | { type: 'setUser', user: {} | undefined } + | { type: 'setToken', token: {} | undefined } | { type: 'setGapi', gapi: PartialDeep | undefined } + | { type: 'setGoogle', google: any } + | { type: 'setTokenClient', tokenClient: any } | { type: 'setToast', toast: string | undefined } | { type: 'gapiStatus', status: string | undefined }; const reducer = (state: State = {}, action: Action) => { switch (action.type) { - case "setUser": - return { ...state, user: action.user } + case "setToken": + return { ...state, token: action.token } case "setGapi": return { ...state, gapi: action.gapi } + case "setGoogle": + return { ...state, google: action.google } + case "setTokenClient": + return { ...state, tokenClient: action.tokenClient } case "setToast": return { ...state, toast: action.toast } case "gapiStatus": diff --git a/package.json b/package.json index f7dd45505..bbc6cc793 100644 --- a/package.json +++ b/package.json @@ -60,11 +60,9 @@ "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^13.1.8", "@types/gapi": "^0.0.39", - "@types/gapi.auth2": "^0.0.54", "@types/gapi.client.analytics": "^3.0.7", "@types/gapi.client.analyticsadmin": "^1.0.0", "@types/gapi.client.analyticsdata": "^1.0.2", - "@types/gapi.client.analyticsreporting": "^4.0.3", "@types/gtag.js": "^0.0.5", "@types/jest": "^26.0.23", "@types/node": "^18.16.5", diff --git a/src/components/Layout/Login.tsx b/src/components/Layout/Login.tsx index 98d19589a..a421315df 100644 --- a/src/components/Layout/Login.tsx +++ b/src/components/Layout/Login.tsx @@ -7,7 +7,7 @@ import { UserStatus } from "./useLogin" interface LoginProps { className?: string - user: gapi.auth2.GoogleUser | undefined + user: any userStatus: UserStatus login: () => void logout: () => void diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index b09a8cb0e..f5c86d326 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -69,7 +69,7 @@ interface TemplateProps { userStatus?: UserStatus login?: () => void logout?: () => void - user?: gapi.auth2.GoogleUser + user?: any } const PREFIX = 'Layout2'; diff --git a/src/components/Layout/useLogin.ts b/src/components/Layout/useLogin.ts index 4ed62af9f..b3be6c423 100644 --- a/src/components/Layout/useLogin.ts +++ b/src/components/Layout/useLogin.ts @@ -1,7 +1,7 @@ import { Requestable, RequestStatus } from "@/types" import { useState, useEffect, useCallback } from "react" -import { useSelector } from "react-redux" +import { useSelector, useDispatch } from "react-redux" export enum UserStatus { SignedIn, @@ -11,7 +11,7 @@ export enum UserStatus { interface Successful { userStatus: UserStatus - user: gapi.auth2.GoogleUser | undefined + user: any logout: () => void login: () => void } @@ -21,24 +21,30 @@ interface Failed { } const useLogin = (): Requestable => { const [requestStatus, setRequestStatus] = useState(RequestStatus.NotStarted) - const user = useSelector((state: AppState) => state.user) + const token = useSelector((state: AppState) => state.token) const gapi = useSelector((state: AppState) => state.gapi) const gapiStatus = useSelector((state: AppState) => state.gapiStatus) - const [userStatus, setUserStatus] = useState(UserStatus.Pending) + const tokenClient = useSelector((state: AppState) => state.tokenClient) + const google = useSelector((state: AppState) => state.google) + const dispatch = useDispatch() + const userStatus = token ? UserStatus.SignedIn : UserStatus.SignedOut const login = useCallback(() => { - if (gapi === undefined) { - return + if (tokenClient) { + tokenClient.requestAccessToken() } - gapi.auth2.getAuthInstance().signIn() - }, [gapi]) + }, [tokenClient]) const logout = useCallback(() => { - if (gapi === undefined) { - return + const token = gapi?.client.getToken() + if (token && google) { + google.accounts.oauth2.revoke(token.access_token, () => { + gapi?.client.setToken(null) + dispatch({ type: "setToken", token: undefined }) + localStorage.removeItem("google_token") + }) } - gapi.auth2.getAuthInstance().signOut() - }, [gapi]) + }, [gapi, google, dispatch]) useEffect(() => { if (gapiStatus === "cannot initialize") { @@ -61,19 +67,11 @@ const useLogin = (): Requestable => { setRequestStatus(RequestStatus.InProgress) } - gapi.auth2.getAuthInstance().isSignedIn.listen(signedIn => { - setUserStatus(signedIn ? UserStatus.SignedIn : UserStatus.SignedOut) - }) - - gapi.auth2.getAuthInstance().isSignedIn.get() - ? setUserStatus(UserStatus.SignedIn) - : setUserStatus(UserStatus.SignedOut) - setRequestStatus(RequestStatus.Successful) }, [gapi, requestStatus, gapiStatus]) if (requestStatus === RequestStatus.Successful) { - return { status: requestStatus, userStatus, user, login, logout } + return { status: requestStatus, userStatus, user: token, login, logout } } if (requestStatus === RequestStatus.Failed) { return { status: requestStatus, message: gapiStatus || "unknown" } diff --git a/src/global.d.ts b/src/global.d.ts index 2e96de320..023f31ab2 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -23,8 +23,10 @@ declare module "*.svg" { declare module "*.module.css"; declare interface AppState { - user?: gapi.auth2.GoogleUser + token?: Token gapi?: typeof gapi gapiStatus?: string measurementID: string + tokenClient?: any + google?: typeof google } diff --git a/src/test-utils/index.tsx b/src/test-utils/index.tsx index 437864dc8..400976a96 100644 --- a/src/test-utils/index.tsx +++ b/src/test-utils/index.tsx @@ -47,9 +47,9 @@ export const wrapperFor = ({ const store = makeStore() if (isLoggedIn) { - store.dispatch({ type: "setUser", user: {} }) + store.dispatch({ type: "setToken", token: {} }) } else { - store.dispatch({ type: "setUser", user: undefined }) + store.dispatch({ type: "setToken", token: undefined }) } const gapi = testGapi(gapiMocks) @@ -91,9 +91,9 @@ export const withProviders = ( const store = makeStore() if (isLoggedIn) { - store.dispatch({ type: "setUser", user: {} }) + store.dispatch({ type: "setToken", token: {} }) } else { - store.dispatch({ type: "setUser", user: undefined }) + store.dispatch({ type: "setToken", token: undefined }) } const gapi = testGapi(gapiMocks) diff --git a/tsconfig.json b/tsconfig.json index db029a2fa..1b43c1926 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -40,11 +40,9 @@ "jest", "@testing-library/jest-dom", "@types/gapi", - "@types/gapi.auth2", "@types/gapi.client.analytics", "@types/gapi.client.analyticsadmin", "@types/gapi.client.analyticsdata", - "@types/gapi.client.analyticsreporting", "@types/gtag.js" ], // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */