Skip to content
Open
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
31 changes: 30 additions & 1 deletion gatsby/onInitialClientRender.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
import loadScript from "load-script"
import { store } from "./wrapRootElement"

// Fetches the signed-in user's name, email & photo. Tokens granted before the
// profile scopes were added get a 401 here, so the profile is just left empty.
const fetchProfile = token => {
fetch("https://www.googleapis.com/oauth2/v3/userinfo", {
headers: { Authorization: `Bearer ${token.access_token}` },
})
.then(response => (response.ok ? response.json() : undefined))
.then(profile => {
if (profile) {
store.dispatch({
type: "setProfile",
profile: {
name: profile.name,
email: profile.email,
picture: profile.picture,
},
})
}
})
.catch(e => console.error("Unable to load the user's profile:", e))
}

export const onInitialClientRender = () => {
const gapiPromise = new Promise((resolve, reject) => {
loadScript("https://apis.google.com/js/api.js", err => {
Expand All @@ -24,7 +46,12 @@ export const onInitialClientRender = () => {

Promise.all([gapiPromise, gisPromise])
.then(([gapi, google]) => {
const SCOPES = ["https://www.googleapis.com/auth/analytics.readonly"]
const SCOPES = [
"https://www.googleapis.com/auth/analytics.readonly",
"openid",
"email",
"profile",
]
const clientId = process.env.GAPI_CLIENT_ID

if (!clientId) {
Expand All @@ -44,6 +71,7 @@ export const onInitialClientRender = () => {
if (storedToken.expires_at > Date.now()) {
gapi.client.setToken(storedToken)
store.dispatch({ type: "setToken", token: storedToken })
fetchProfile(storedToken)
} else {
localStorage.removeItem("google_token")
}
Expand Down Expand Up @@ -75,6 +103,7 @@ export const onInitialClientRender = () => {
localStorage.setItem("google_token", JSON.stringify(tokenWithExpiry))
gapi.client.setToken(tokenResponse)
store.dispatch({ type: "setToken", token: tokenResponse })
fetchProfile(tokenResponse)
} else {
store.dispatch({ type: "setToken", token: undefined })
}
Expand Down
11 changes: 9 additions & 2 deletions gatsby/wrapRootElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,23 @@ type State =
tokenClient?: any,
toast?: string,
gapiStatus?: string
profile?: Profile
}

type Action =
| { type: 'setToken', token: {} | undefined }
| { type: 'setGapi', gapi: PartialDeep<typeof gapi> | undefined }
| { type: 'setTokenClient', tokenClient: any }
| { type: 'setToast', toast: string | undefined }
| { type: 'gapiStatus', status: string | undefined };
| { type: 'gapiStatus', status: string | undefined }
| { type: 'setProfile', profile: Profile | undefined };
const reducer = (state: State = {}, action: Action) => {
switch (action.type) {
case "setToken":
return { ...state, token: action.token }
// Signing out also clears the profile.
return action.token
? { ...state, token: action.token }
: { ...state, token: undefined, profile: undefined }
case "setGapi":
return { ...state, gapi: action.gapi }
case "setTokenClient":
Expand All @@ -43,6 +48,8 @@ const reducer = (state: State = {}, action: Action) => {
return { ...state, toast: action.toast }
case "gapiStatus":
return { ...state, gapiStatus: action.status }
case "setProfile":
return { ...state, profile: action.profile }

default:
return state
Expand Down
77 changes: 69 additions & 8 deletions src/components/Layout/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,39 @@
import * as React from "react"
import { styled } from "@mui/material/styles"
import PersonOutlineIcon from "@mui/icons-material/PersonOutline"
import PersonIcon from "@mui/icons-material/Person"
import Avatar from "@mui/material/Avatar"
import Tooltip from "@mui/material/Tooltip"
import IconButton from "@mui/material/IconButton"
import Typography from "@mui/material/Typography"
import { UserStatus } from "./useLogin"

const Root = styled("div")(({ theme }) => ({
display: "flex",
alignItems: "center",
minWidth: 0,
"& .details": {
display: "flex",
flexDirection: "column",
alignItems: "flex-end",
marginRight: theme.spacing(0.5),
minWidth: 0,
"& > *": {
maxWidth: "28ch",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
lineHeight: 1.3,
},
},
}))

interface LoginProps {
className?: string
user: any
profile?: Profile
// Show the user's name & email next to their avatar.
showDetails?: boolean
userStatus: UserStatus
login: () => void
logout: () => void
Expand All @@ -16,22 +42,57 @@ interface LoginProps {
const Login: React.FC<LoginProps> = ({
className,
user,
profile,
showDetails,
userStatus,
login,
logout,
}) => {
if (userStatus === UserStatus.Pending) {
return null
}

if (user === undefined) {
return (
<Tooltip title="Login">
<IconButton className={className} onClick={login}>
<PersonOutlineIcon />
</IconButton>
</Tooltip>
)
}

const tooltip = profile?.email ? `Logout ${profile.email}` : "Logout"

return (
<Tooltip title={user === undefined ? "Login" : "Logout"}>
<IconButton
className={className}
onClick={user === undefined ? login : logout}
>
{user === undefined ? <PersonOutlineIcon /> : <PersonIcon />}
</IconButton>
</Tooltip>
<Root className={className}>
{showDetails && (profile?.name || profile?.email) && (
<div className="details">
{profile.name && (
<Typography variant="body2">{profile.name}</Typography>
)}
{profile.email && (
<Typography variant="caption" color="text.secondary">
{profile.email}
</Typography>
)}
</div>
)}
<Tooltip title={tooltip}>
<IconButton onClick={logout} aria-label="Logout">
{profile?.picture ? (
<Avatar
src={profile.picture}
alt={profile.name || ""}
imgProps={{ referrerPolicy: "no-referrer" }}
sx={{ width: 32, height: 32 }}
/>
) : (
<PersonIcon />
)}
</IconButton>
</Tooltip>
</Root>
)
}

Expand Down
5 changes: 5 additions & 0 deletions src/components/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ interface TemplateProps {
login?: () => void
logout?: () => void
user?: any
profile?: Profile
}

const PREFIX = 'Layout2';
Expand Down Expand Up @@ -115,6 +116,7 @@ const Template: React.FC<PropsWithChildren<LayoutProps & TemplateProps>> = ({
login,
logout,
user,
profile,
}) => {
usePageView(title)
const { gaVersion, setGAVersion } = useGAVersion(pathname)
Expand Down Expand Up @@ -178,6 +180,7 @@ const Template: React.FC<PropsWithChildren<LayoutProps & TemplateProps>> = ({
{successful && (
<Login
user={user!}
profile={profile}
login={login!}
logout={logout!}
userStatus={userStatus!}
Expand Down Expand Up @@ -277,7 +280,9 @@ const Template: React.FC<PropsWithChildren<LayoutProps & TemplateProps>> = ({
<BugReport />
{!disableNav && successful && (
<Login
showDetails
user={user!}
profile={profile}
login={login!}
logout={logout!}
userStatus={userStatus!}
Expand Down
11 changes: 10 additions & 1 deletion src/components/Layout/useLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export enum UserStatus {
interface Successful {
userStatus: UserStatus
user: any
profile?: Profile
logout: () => void
login: () => void
}
Expand All @@ -25,6 +26,7 @@ const useLogin = (): Requestable<Successful, {}, InProgress, Failed> => {
const gapi = useSelector((state: AppState) => state.gapi)
const gapiStatus = useSelector((state: AppState) => state.gapiStatus)
const tokenClient = useSelector((state: AppState) => state.tokenClient)
const profile = useSelector((state: AppState) => state.profile)
const dispatch = useDispatch()
const userStatus = token ? UserStatus.SignedIn : UserStatus.SignedOut

Expand Down Expand Up @@ -65,7 +67,14 @@ const useLogin = (): Requestable<Successful, {}, InProgress, Failed> => {
}, [gapi, requestStatus, gapiStatus])

if (requestStatus === RequestStatus.Successful) {
return { status: requestStatus, userStatus, user: token, login, logout }
return {
status: requestStatus,
userStatus,
user: token,
profile,
login,
logout,
}
}
if (requestStatus === RequestStatus.Failed) {
return { status: requestStatus, message: gapiStatus || "unknown" }
Expand Down
7 changes: 7 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,11 @@ declare interface AppState {
gapiStatus?: string
measurementID: string
tokenClient?: any
profile?: Profile
}

declare interface Profile {
name?: string
email?: string
picture?: string
}
Loading