Add the new Google Sign In to your React app!
Tags: react, google, signin, auth
Published: 2021-08-17 22:10:15

*TLDR: Scroll down and copy the code. You only need to add your login logic.*

This article will cover: 
- A brief introduction to the new-ish Google Sign In api
- How to implement it using React and Typescript
  - Add relevant typings on global window object
  
## Intro
Google recently announced they are discontinuing their old auth-service "Google Sign-In" in favor for their new and improved service "Sign In With Google".

Their new service comes in two parts:
1. Login button
2. One Tap

You can read more about them [here](https://developers.google.com/identity/gsi/web). 
We'll cover the first one in this article, how it works and how to implement it in React with Typescript. 

Compared to the old service, this one is much easier to use. It's straight-forward enough to implement the login button yourself without needing a library like (the awesome) `react-google-login` that's the go-to solution for the old api. 

## Google Auth Introduction
I'm just going to go over the basics here. 

*Disclaimer: There might be a much better way to do this. I would be happy to know how, so leave a comment! I couldn't find any examples of this, so I figured I'd post my implementation and hopefully help someone else.*

Although the new auth api is a bit tricky to get your head around at first when using React, we can make it work. The trick is to understand how the script loads the client and how that fits with React's loading and rendering. 

The google documentation covers both the html and javascript api, and we'll be using the latter. But since we're building with React, we mostly use the step-by-step guide to figure out how the auth api works. We have to account for how React loads and renders elements. Unfortunately this means we can't just statically stick it in the header like the guide instructs. 

After you followed the [setup process](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid), the documentation tells you to add a script tag to your header (in `public/index.html`), but since we're using React we're **not** going to do that. We're going to control when and where we run that script, and thus initiate the google auth client. We're doing this because the script initiates a client and we want to pass it our own callback function that we define with react.
```jsx 
// The script that runs and load the new google auth client.
// We're not(!) adding it to our header like the guide says.
<script src="https://accounts.google.com/gsi/client" async defer></script>
```

### Lets get started
First off, Typescript will complain about missing types on the `window` object. We'll fix that properly later.

What we'll implement first is adding the script that loads the google auth client when our sign-in page renders, add the "target div" that the script will be looking for, and initiate the client with our callback function. 

## The problem
Attaching that callback-function to the google client is what makes using the new auth api with React a bit troublesome. (but even more so using the old one!). If we add the script tag to the static html like the docs say, we can't pass it any function defined in react. We could maybe handle stuff by defining a function on the server-side of things, but I want to stay within React and handle this on the front-end and use my graphql-hooks to login.

## The process
When our login page renders, we'll attach the google client-script to the header from inside a `useEffect` hook. We'll add an initializer-function to the `onLoad`-eventlistener for that script tag. The onLoad event will then trigger and initialize the google auth client with our callback attached.

The google client will then magically find our already rendered `div` with `id=g_id_signin` and render the login-button. 

A nice looking, personalized google sign-in button should now be visible to the user. 

![alt text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tjg7uyljrdx79u3mxx9k.png)

## The code  

```ts
import { Button } from "@material-ui/core"
import { useEffect, useState } from "react"

export default function GoogleSignin() {
  const [gsiScriptLoaded, setGsiScriptLoaded] = useState(false)
  const [user, setUser] = useState(undefined)

  useEffect(() => {
    if (user?._id || gsiScriptLoaded) return

    const initializeGsi = () => {
      // Typescript will complain about window.google
      // Add types to your `react-app-env.d.ts` or //@ts-ignore it.
      if (!window.google || gsiScriptLoaded) return
    
      setGsiScriptLoaded(true)
      window.google.accounts.id.initialize({
        client_id: GOOGLE_CLIENT_ID,
        callback: handleGoogleSignIn,
      })
    }

    const script = document.createElement("script")
    script.src = "https://accounts.google.com/gsi/client"
    script.onload = initializeGsi
    script.async = true
    script.id = "google-client-script"
    document.querySelector("body")?.appendChild(script)

    return () => {
      // Cleanup function that runs when component unmounts
      window.google?.accounts.id.cancel()
      document.getElementById("google-client-script")?.remove()
    }
  }, [handleGoogleSignIn, initializeGsi, user?._id])
 



const handleGoogleSignIn = (res: CredentialResponse) => {
  if (!res.clientId || !res.credential) return

    // Implement your login mutations and logic here.
    // Set cookies, call your backend, etc. 

    setUser(val.data?.login.user)
  })
}

return <Button className={"g_id_signin"} />

}
```

You might want to add some more implementation details here and there. But this is the gist of it! You can at least use it as a starting point. Hope it helps! 

## Fixing the window types
If you're using `create-react-app`, you will already have the file `react-app-env.d.ts` in your project root. You can add the types for the google auth api there. I translated the api documentation to typescript types. There might be some errors since I haven't used and tested all the functions. But it *should* be correct.
```ts 
/// <reference types="react-scripts" />

interface IdConfiguration {
  client_id: string
  auto_select?: boolean
  callback: (handleCredentialResponse: CredentialResponse) => void
  login_uri?: string
  native_callback?: Function
  cancel_on_tap_outside?: boolean
  prompt_parent_id?: string
  nonce?: string
  context?: string
  state_cookie_domain?: string
  ux_mode?: "popup" | "redirect"
  allowed_parent_origin?: string | string[]
  intermediate_iframe_close_callback?: Function
}

interface CredentialResponse {
  credential?: string
  select_by?:
    | "auto"
    | "user"
    | "user_1tap"
    | "user_2tap"
    | "btn"
    | "btn_confirm"
    | "brn_add_session"
    | "btn_confirm_add_session"
  clientId?: string
}

interface GsiButtonConfiguration {
  type: "standard" | "icon"
  theme?: "outline" | "filled_blue" | "filled_black"
  size?: "large" | "medium" | "small"
  text?: "signin_with" | "signup_with" | "continue_with" | "signup_with"
  shape?: "rectangular" | "pill" | "circle" | "square"
  logo_alignment?: "left" | "center"
  width?: string
  local?: string
}

interface PromptMomentNotification {
  isDisplayMoment: () => boolean
  isDisplayed: () => boolean
  isNotDisplayed: () => boolean
  getNotDisplayedReason: () =>
    | "browser_not_supported"
    | "invalid_client"
    | "missing_client_id"
    | "opt_out_or_no_session"
    | "secure_http_required"
    | "suppressed_by_user"
    | "unregistered_origin"
    | "unknown_reason"
  isSkippedMoment: () => boolean
  getSkippedReason: () =>
    | "auto_cancel"
    | "user_cancel"
    | "tap_outside"
    | "issuing_failed"
  isDismissedMoment: () => boolean
  getDismissedReason: () =>
    | "credential_returned"
    | "cancel_called"
    | "flow_restarted"
  getMomentType: () => "display" | "skipped" | "dismissed"
}
interface Window {
  google?: {
    accounts: {
      id: {
        initialize: (input: IdConfiguration) => void
        prompt: (
          momentListener: (res: PromptMomentNotification) => void
        ) => void
        renderButton: (
          parent: HTMLElement,
          options: GsiButtonConfiguration,
          clickHandler: Function
        ) => void
        disableAutoSelect: Function
        storeCredential: Function<{
          credentials: { id: string; password: string }
          callback: Function
        }>
        cancel: () => void
        onGoogleLibraryLoad: Function
        revoke: Function<{
          hint: string
          callback: Function<{ successful: boolean; error: string }>
        }>
      }
    }
  }
}

```

### Shameless plug
If you like this kind of stuff and are looking for a job in Sweden, Gothenburg, hit me up!