Skip to content

Commit 57277fc

Browse files
chore(Auth): migrate to Google Identity Services library for login (#2257)
* initial commit * rename user to token, save login state in browser * cleanup * remove header * remove gapi types * remove console.log messages * cleanup
1 parent 73b88ac commit 57277fc

File tree

9 files changed

+124
-83
lines changed

9 files changed

+124
-83
lines changed

gatsby/onInitialClientRender.js

Lines changed: 83 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,61 +2,98 @@ import loadScript from "load-script"
22
import { store } from "./wrapRootElement"
33

44
export const onInitialClientRender = () => {
5-
loadScript(`https://apis.google.com/js/api.js`, err => {
6-
if (err) {
7-
console.error("Could not load gapi")
8-
return
9-
}
5+
const gapiPromise = new Promise((resolve, reject) => {
6+
loadScript("https://apis.google.com/js/api.js", err => {
7+
if (err) {
8+
reject(err)
9+
} else {
10+
window.gapi.load("client", () => {
11+
resolve(window.gapi)
12+
})
13+
}
14+
})
15+
})
16+
17+
const gisPromise = new Promise((resolve, reject) => {
18+
loadScript("https://accounts.google.com/gsi/client", err => {
19+
if (err) {
20+
reject(err)
21+
} else resolve(window.google)
22+
})
23+
})
24+
25+
Promise.all([gapiPromise, gisPromise])
26+
.then(([gapi, google]) => {
27+
const SCOPES = ["https://www.googleapis.com/auth/analytics.readonly"]
28+
const clientId = process.env.GAPI_CLIENT_ID
1029

11-
var SCOPES = ["https://www.googleapis.com/auth/analytics.readonly"]
30+
if (!clientId) {
31+
console.error(
32+
"GAPI_CLIENT_ID is not defined. Please check your .env file."
33+
)
34+
store.dispatch({ type: "gapiStatus", status: "cannot initialize" })
35+
return
36+
}
1237

13-
const clientId = process.env.GAPI_CLIENT_ID
38+
// Google Sign-In previously helped manage user signed-in status
39+
// With GIS we are responsible for managing sign-in state
40+
try {
41+
const storedTokenString = localStorage.getItem("google_token")
42+
if (storedTokenString) {
43+
const storedToken = JSON.parse(storedTokenString)
44+
if (storedToken.expires_at > Date.now()) {
45+
gapi.client.setToken(storedToken)
46+
store.dispatch({ type: "setToken", token: storedToken })
47+
} else {
48+
localStorage.removeItem("google_token")
49+
}
50+
}
51+
} catch (e) {
52+
console.error("Unable to restore token from localStorage:", e)
53+
localStorage.removeItem("google_token")
54+
}
1455

15-
// TODO - Remove :analytics and replace it with the discovery document.
16-
window.gapi.load("client:auth2:analytics", () => {
1756
Promise.all([
18-
window.gapi.client.load(
19-
"https://analyticsreporting.googleapis.com/$discovery/rest?version=v4"
20-
),
21-
window.gapi.client.load(
57+
gapi.client.load(
2258
"https://analyticsdata.googleapis.com/$discovery/rest"
2359
),
24-
window.gapi.client.load(
60+
gapi.client.load(
2561
"https://analyticsadmin.googleapis.com/$discovery/rest"
2662
),
27-
]).then(() => {
28-
window.gapi.client
29-
.init({
63+
])
64+
.then(() => {
65+
// Replace gapi.auth2.init() with google.accounts.oauth2.initTokenClient()
66+
const tokenClient = google.accounts.oauth2.initTokenClient({
67+
client_id: clientId,
3068
scope: SCOPES.join(" "),
31-
clientId,
32-
})
33-
.then(() => {
34-
store.dispatch({ type: "setGapi", gapi: window.gapi })
35-
const user = window.gapi.auth2.getAuthInstance().currentUser.get()
36-
store.dispatch({
37-
type: "setUser",
38-
user: user.isSignedIn() ? user : undefined,
39-
})
40-
window.gapi.auth2.getAuthInstance().currentUser.listen(user => {
41-
store.dispatch({
42-
type: "setUser",
43-
user: user.isSignedIn() ? user : undefined,
44-
})
45-
})
46-
})
47-
.catch(e => {
48-
store.dispatch({ type: "setGapi", gapi: window.gapi })
49-
store.dispatch({
50-
type: "setUser",
51-
user: undefined,
52-
})
53-
store.dispatch({
54-
type: "gapiStatus",
55-
status: "cannot initialize",
56-
})
57-
console.error(e)
69+
callback: tokenResponse => {
70+
if (tokenResponse && tokenResponse.access_token) {
71+
const tokenWithExpiry = {
72+
...tokenResponse,
73+
expires_at: Date.now() + tokenResponse.expires_in * 1000,
74+
}
75+
localStorage.setItem("google_token", JSON.stringify(tokenWithExpiry))
76+
gapi.client.setToken(tokenResponse)
77+
store.dispatch({ type: "setToken", token: tokenResponse })
78+
} else {
79+
store.dispatch({ type: "setToken", token: undefined })
80+
}
81+
}
5882
})
59-
}, console.error)
83+
84+
store.dispatch({ type: "setGapi", gapi })
85+
store.dispatch({ type: "gapiStatus", status: "initialized" })
86+
store.dispatch({ type: "setGoogle", google })
87+
store.dispatch({ type: "setTokenClient", tokenClient })
88+
})
89+
.catch(e => {
90+
store.dispatch({ type: "setGapi", gapi })
91+
store.dispatch({ type: "setToken", token: undefined })
92+
store.dispatch({ type: "gapiStatus", status: "cannot initialize" })
93+
console.error(e)
94+
})
95+
})
96+
.catch(e => {
97+
console.error(e)
6098
})
61-
})
6299
}

gatsby/wrapRootElement.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,31 @@ import {PartialDeep} from 'type-fest';
1818

1919
type State =
2020
{
21-
user?: {},
21+
token?: {},
2222
gapi?: PartialDeep<typeof gapi>,
23+
google?: any,
24+
tokenClient?: any,
2325
toast?: string,
24-
status?: string
26+
gapiStatus?: string
2527
}
2628

2729
type Action =
28-
| { type: 'setUser', user: {} | undefined }
30+
| { type: 'setToken', token: {} | undefined }
2931
| { type: 'setGapi', gapi: PartialDeep<typeof gapi> | undefined }
32+
| { type: 'setGoogle', google: any }
33+
| { type: 'setTokenClient', tokenClient: any }
3034
| { type: 'setToast', toast: string | undefined }
3135
| { type: 'gapiStatus', status: string | undefined };
3236
const reducer = (state: State = {}, action: Action) => {
3337
switch (action.type) {
34-
case "setUser":
35-
return { ...state, user: action.user }
38+
case "setToken":
39+
return { ...state, token: action.token }
3640
case "setGapi":
3741
return { ...state, gapi: action.gapi }
42+
case "setGoogle":
43+
return { ...state, google: action.google }
44+
case "setTokenClient":
45+
return { ...state, tokenClient: action.tokenClient }
3846
case "setToast":
3947
return { ...state, toast: action.toast }
4048
case "gapiStatus":

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,9 @@
6060
"@testing-library/react": "^14.0.0",
6161
"@testing-library/user-event": "^13.1.8",
6262
"@types/gapi": "^0.0.39",
63-
"@types/gapi.auth2": "^0.0.54",
6463
"@types/gapi.client.analytics": "^3.0.7",
6564
"@types/gapi.client.analyticsadmin": "^1.0.0",
6665
"@types/gapi.client.analyticsdata": "^1.0.2",
67-
"@types/gapi.client.analyticsreporting": "^4.0.3",
6866
"@types/gtag.js": "^0.0.5",
6967
"@types/jest": "^26.0.23",
7068
"@types/node": "^18.16.5",

src/components/Layout/Login.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { UserStatus } from "./useLogin"
77

88
interface LoginProps {
99
className?: string
10-
user: gapi.auth2.GoogleUser | undefined
10+
user: any
1111
userStatus: UserStatus
1212
login: () => void
1313
logout: () => void

src/components/Layout/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ interface TemplateProps {
6969
userStatus?: UserStatus
7070
login?: () => void
7171
logout?: () => void
72-
user?: gapi.auth2.GoogleUser
72+
user?: any
7373
}
7474

7575
const PREFIX = 'Layout2';

src/components/Layout/useLogin.ts

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Requestable, RequestStatus } from "@/types"
22
import { useState, useEffect, useCallback } from "react"
33

4-
import { useSelector } from "react-redux"
4+
import { useSelector, useDispatch } from "react-redux"
55

66
export enum UserStatus {
77
SignedIn,
@@ -11,7 +11,7 @@ export enum UserStatus {
1111

1212
interface Successful {
1313
userStatus: UserStatus
14-
user: gapi.auth2.GoogleUser | undefined
14+
user: any
1515
logout: () => void
1616
login: () => void
1717
}
@@ -21,24 +21,30 @@ interface Failed {
2121
}
2222
const useLogin = (): Requestable<Successful, {}, InProgress, Failed> => {
2323
const [requestStatus, setRequestStatus] = useState(RequestStatus.NotStarted)
24-
const user = useSelector((state: AppState) => state.user)
24+
const token = useSelector((state: AppState) => state.token)
2525
const gapi = useSelector((state: AppState) => state.gapi)
2626
const gapiStatus = useSelector((state: AppState) => state.gapiStatus)
27-
const [userStatus, setUserStatus] = useState<UserStatus>(UserStatus.Pending)
27+
const tokenClient = useSelector((state: AppState) => state.tokenClient)
28+
const google = useSelector((state: AppState) => state.google)
29+
const dispatch = useDispatch()
30+
const userStatus = token ? UserStatus.SignedIn : UserStatus.SignedOut
2831

2932
const login = useCallback(() => {
30-
if (gapi === undefined) {
31-
return
33+
if (tokenClient) {
34+
tokenClient.requestAccessToken()
3235
}
33-
gapi.auth2.getAuthInstance().signIn()
34-
}, [gapi])
36+
}, [tokenClient])
3537

3638
const logout = useCallback(() => {
37-
if (gapi === undefined) {
38-
return
39+
const token = gapi?.client.getToken()
40+
if (token && google) {
41+
google.accounts.oauth2.revoke(token.access_token, () => {
42+
gapi?.client.setToken(null)
43+
dispatch({ type: "setToken", token: undefined })
44+
localStorage.removeItem("google_token")
45+
})
3946
}
40-
gapi.auth2.getAuthInstance().signOut()
41-
}, [gapi])
47+
}, [gapi, google, dispatch])
4248

4349
useEffect(() => {
4450
if (gapiStatus === "cannot initialize") {
@@ -61,19 +67,11 @@ const useLogin = (): Requestable<Successful, {}, InProgress, Failed> => {
6167
setRequestStatus(RequestStatus.InProgress)
6268
}
6369

64-
gapi.auth2.getAuthInstance().isSignedIn.listen(signedIn => {
65-
setUserStatus(signedIn ? UserStatus.SignedIn : UserStatus.SignedOut)
66-
})
67-
68-
gapi.auth2.getAuthInstance().isSignedIn.get()
69-
? setUserStatus(UserStatus.SignedIn)
70-
: setUserStatus(UserStatus.SignedOut)
71-
7270
setRequestStatus(RequestStatus.Successful)
7371
}, [gapi, requestStatus, gapiStatus])
7472

7573
if (requestStatus === RequestStatus.Successful) {
76-
return { status: requestStatus, userStatus, user, login, logout }
74+
return { status: requestStatus, userStatus, user: token, login, logout }
7775
}
7876
if (requestStatus === RequestStatus.Failed) {
7977
return { status: requestStatus, message: gapiStatus || "unknown" }

src/global.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ declare module "*.svg" {
2323
declare module "*.module.css";
2424

2525
declare interface AppState {
26-
user?: gapi.auth2.GoogleUser
26+
token?: Token
2727
gapi?: typeof gapi
2828
gapiStatus?: string
2929
measurementID: string
30+
tokenClient?: any
31+
google?: typeof google
3032
}

src/test-utils/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ export const wrapperFor = ({
4747
const store = makeStore()
4848

4949
if (isLoggedIn) {
50-
store.dispatch({ type: "setUser", user: {} })
50+
store.dispatch({ type: "setToken", token: {} })
5151
} else {
52-
store.dispatch({ type: "setUser", user: undefined })
52+
store.dispatch({ type: "setToken", token: undefined })
5353
}
5454

5555
const gapi = testGapi(gapiMocks)
@@ -91,9 +91,9 @@ export const withProviders = (
9191
const store = makeStore()
9292

9393
if (isLoggedIn) {
94-
store.dispatch({ type: "setUser", user: {} })
94+
store.dispatch({ type: "setToken", token: {} })
9595
} else {
96-
store.dispatch({ type: "setUser", user: undefined })
96+
store.dispatch({ type: "setToken", token: undefined })
9797
}
9898

9999
const gapi = testGapi(gapiMocks)

tsconfig.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,9 @@
4040
"jest",
4141
"@testing-library/jest-dom",
4242
"@types/gapi",
43-
"@types/gapi.auth2",
4443
"@types/gapi.client.analytics",
4544
"@types/gapi.client.analyticsadmin",
4645
"@types/gapi.client.analyticsdata",
47-
"@types/gapi.client.analyticsreporting",
4846
"@types/gtag.js"
4947
],
5048
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */

0 commit comments

Comments
 (0)