49

I'm trying to implement Google Sign In and retrieve the profile information of the user. The error is: Uncaught ReferenceError: gapi is not defined. Why is that?

<!doctype html>
<html>
<head>      
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="https://apis.google.com/js/platform.js" async defer></script>
    <script type="text/javascript">
    $(function(){
        gapi.auth2.init({
            client_id: 'filler_text_for_client_id.apps.googleusercontent.com'
        });
    });
</head>
<body>
</body>
</html>
Jon Tan
  • 1,461
  • 3
  • 17
  • 33

7 Answers7

58

It happens because you have async and defer attributes on your script tag. gapi would be loaded after your script tag with gapi.auth2.init...

To wait for gapi before executing this code you can use onload query param in script tag, like following:

<script src="https://apis.google.com/js/platform.js?onload=onLoadCallback" async defer></script>
<script>
window.onLoadCallback = function(){
  gapi.auth2.init({
      client_id: 'filler_text_for_client_id.apps.googleusercontent.com'
    });
}
</script>

Or for case when you need it in many places, you can use promises to better structure it:

// promise that would be resolved when gapi would be loaded
var gapiPromise = (function(){
  var deferred = $.Deferred();
  window.onLoadCallback = function(){
    deferred.resolve(gapi);
  };
  return deferred.promise()
}());

var authInited = gapiPromise.then(function(){
  gapi.auth2.init({
      client_id: 'filler_text_for_client_id.apps.googleusercontent.com'
    });
})


$('#btn').click(function(){
  gapiPromise.then(function(){
    // will be executed after gapi is loaded
  });

  authInited.then(function(){
    // will be executed after gapi is loaded, and gapi.auth2.init was called
  });
});
Bogdan Savluk
  • 6,274
  • 1
  • 30
  • 36
  • where should I add the onSignOut function then? It also requires gapi, but I only call it when the user clicks on the button – Jon Tan Jun 30 '15 at 18:21
  • you can add it to `onLoadCallback` too, e.g place `$(function(){ .... })` there.. – Bogdan Savluk Jun 30 '15 at 18:27
  • That was the first thing I tried. Sorry I didn't mention it before. Trying to call signOut results in a "Uncaught ReferenceError: signOut is not defined" – Jon Tan Jun 30 '15 at 18:31
  • OH wait, nevermind. you're talking about the document.ready function lol. wait a second, let me try it. – Jon Tan Jun 30 '15 at 18:32
  • added example how to structure this with promises – Bogdan Savluk Jun 30 '15 at 18:41
  • I did all this, and even added a interval to check if it's loaded, but still had problems. Finally I removed the async in the script tag and since that it seems to work. – arpo Sep 19 '16 at 08:55
  • the first one worked for me, I just get this error (I'm using Angular2) ```zone.js:1235 Uncaught TypeError: Cannot redefine property: onLoadCallback``` – Nick Jonas Apr 13 '17 at 21:36
  • This did not work for me. Anyone looking for a solution that works if you're using Vue with a separate log out component, refer to @bora89's answer below. – Soubriquet Jun 29 '19 at 22:10
19

I think you'll find with the above example that this won't work either, as gapi.auth2 will not yet be defined (I know this because I made the same mistake myself, today) You first need to call gapi.load('auth2', callback) and pass THAT a callback which then calls gapi.auth2.init. Here is an example of my _onGoogleLoad function, which is the callback for loading the first platform.js script.

var _auth2

var _onGoogleLoad = function () {
  gapi.load('auth2', function () {
    _auth2 = gapi.auth2.init({
      client_id: 'OUR_REAL_ID_GOES_HERE',
      scope: 'email',
      fetch_basic_profile: false
    })
    _enableGoogleButton()
  })
}

After that, you can use the _auth2 variable to actually sign users in.

simeg
  • 1,889
  • 2
  • 26
  • 34
Kelly Ellis
  • 1,081
  • 1
  • 11
  • 13
  • Thanks for your answer, I'll try it! – Jon Tan Jul 02 '15 at 22:46
  • OR you include a `
    ` (etc..) button as described in the docs. When present, platform.js will automatically download `auth2`. This seems to trigger the exact same content being downloaded. The documentation is not very good in this regard, unfortunately. Also, the `
    – phil294 Apr 30 '18 at 17:00
  • @phil294 is there a way to suppress that behaviour ? – beppe9000 Jun 22 '20 at 13:40
  • @beppe9000 I dont know, sorry – phil294 Jun 22 '20 at 14:07
18

The problem is not only in gapi. To call init method - auth2 object must be initialized. There is a promise once a google auth object is in fully initialized GoogleAuth.then(onInit, onFailure)

gapi.load('auth2', initSigninV2);

function initSigninV2() {
    gapi.auth2.init({
        client_id: 'CLIENT_ID.apps.googleusercontent.com'
    }).then(function (authInstance) {
        // now auth2 is fully initialized
    });
}
bora89
  • 3,476
  • 1
  • 25
  • 16
6

While these answers helped me, I believe there is better answer in official docs.

See Integrating Google Sign-In using listeners

var auth2; // The Sign-In object.
var googleUser; // The current user.

/**
 * Calls startAuth after Sign in V2 finishes setting up.
 */
var appStart = function() {
  gapi.load('auth2', initSigninV2);
};

/**
 * Initializes Signin v2 and sets up listeners.
 */
var initSigninV2 = function() {
  auth2 = gapi.auth2.init({
      client_id: 'CLIENT_ID.apps.googleusercontent.com',
      scope: 'profile'
  });

  // Listen for sign-in state changes.
  auth2.isSignedIn.listen(signinChanged);

  // Listen for changes to current user.
  auth2.currentUser.listen(userChanged);

  // Sign in the user if they are currently signed in.
  if (auth2.isSignedIn.get() == true) {
    auth2.signIn();
  }

  // Start with the current live values.
  refreshValues();
};

/**
 * Listener method for sign-out live value.
 *
 * @param {boolean} val the updated signed out state.
 */
var signinChanged = function (val) {
  console.log('Signin state changed to ', val);
  document.getElementById('signed-in-cell').innerText = val;
};

/**
 * Listener method for when the user changes.
 *
 * @param {GoogleUser} user the updated user.
 */
var userChanged = function (user) {
  console.log('User now: ', user);
  googleUser = user;
  updateGoogleUser();
  document.getElementById('curr-user-cell').innerText =
    JSON.stringify(user, undefined, 2);
};

/**
 * Updates the properties in the Google User table using the current user.
 */
var updateGoogleUser = function () {
  if (googleUser) {
    document.getElementById('user-id').innerText = googleUser.getId();
    document.getElementById('user-scopes').innerText =
      googleUser.getGrantedScopes();
    document.getElementById('auth-response').innerText =
      JSON.stringify(googleUser.getAuthResponse(), undefined, 2);
  } else {
    document.getElementById('user-id').innerText = '--';
    document.getElementById('user-scopes').innerText = '--';
    document.getElementById('auth-response').innerText = '--';
  }
};

/**
 * Retrieves the current user and signed in states from the GoogleAuth
 * object.
 */
var refreshValues = function() {
  if (auth2){
    console.log('Refreshing values...');

    googleUser = auth2.currentUser.get();

    document.getElementById('curr-user-cell').innerText =
      JSON.stringify(googleUser, undefined, 2);
    document.getElementById('signed-in-cell').innerText =
      auth2.isSignedIn.get();

    updateGoogleUser();
  }
}
Miha Pirnat
  • 111
  • 1
  • 7
4

For a Vue.JS app

  • Add an onload hook to the script tag for platform.js
  • Set up a function that dispatches an event
  • Catch that event in the mounted hook of your component.

Here's a fairly complete example you can just paste into a new vue-cli project.

Don't forget to supply your own client ID!

public/index.html

    <script type="text/javascript">
      function triggerGoogleLoaded() {
        window.dispatchEvent(new Event("google-loaded"));
      }
    </script>
    <script
      src="https://apis.google.com/js/platform.js?onload=triggerGoogleLoaded"
      async
      defer
    ></script>
    <meta
      name="google-signin-client_id"
      content="xxxxxxxxxxxx.apps.googleusercontent.com"
    />

App.vue

<template>
  <div id="app">
    <div id="nav">
      <div id="google-signin-btn"></div>
      <a href="#" class="sign-out" @click="signOut" v-if="profile">Sign out</a>
    </div>
    <div v-if="profile" class="">
      <h2>Signed In User Profile</h2>
      <pre>{{ profile }}</pre>
    </div>
    <div v-if="!profile">
      <h2>Signed out.</h2>
    </div>
    <router-view />
  </div>
</template>

<script>
export default {
  components: {},
  data() {
    return {
      profile: false
    };
  },
  methods: {
    onSignIn(user) {
      const profile = user.getBasicProfile();
      this.profile = profile;
    },
    signOut() {
      var auth2 = gapi.auth2.getAuthInstance();
      auth2.signOut().then(() => {
        location.reload(true);
      });
    },
    renderGoogleLoginButton() {
      gapi.signin2.render("google-signin-btn", {
        onsuccess: this.onSignIn
      });
    }
  },
  mounted() {
    window.addEventListener("google-loaded", this.renderGoogleLoginButton);
  }
};
</script>


counterbeing
  • 2,721
  • 2
  • 27
  • 47
1

This worked for me: https://stackoverflow.com/a/55314602/1034622

Include this script tag

<script src="https://apis.google.com/js/platform.js"></script>
Pablo Chvx
  • 1,809
  • 18
  • 31
-3

Cheap Fix is: To tell Eslint that this is a global variable by using /* global gapi */

  • 1
    This question doesn't have anything to do with eslint, and the error isn't coming from eslint; it's a JavaScript runtime error. – zcoop98 Sep 17 '21 at 21:52