google / google-api-javascript-client

Google APIs Client Library for browser JavaScript, aka gapi.
Apache License 2.0
3.21k stars 1.06k forks source link

GAPI is not always initialised after load #399

Open mesqueeb opened 6 years ago

mesqueeb commented 6 years ago

I have (finally) noticed one of the biggest problems I have. The GAPI client is often not initialised, but hangs just after loading. This is my setup, I'll mark where it hangs:

        window.gapi.load('client:auth2', _ => {
          console.log('loaded GAPI')
          // hangs here
          window.gapi.client.init(gapiConfig)
          .then(_ => {
            console.log('initialised GAPI')
            window.GAPIiniOK = true
            gapi.auth2.getAuthInstance().isSignedIn
              .listen(store.dispatch('gapiAuthListener'))
            // Not required anymore if global auth listener does its job!
            store.dispatch('user/updateAuthStatus')
            .then(_ => {
              resolve()
            })
          }).catch(error => {
            dispatch('authenticationError', {error, note: 'error during gapi initialisation'})
            reject()
          })
        })

It hangs right where I wrote the comment. But the load worked, but the client.init is not launched. I even added a catch statement, but it doesn't get launched.

Does anyone have any idea why?

TMSCH commented 6 years ago

@mesqueeb we observed this issue when injecting the api.js script asynchronously, in some browsers. We're not sure yet what the root cause of the bug is, but in the meantime you can use a setTimeout before the gapi.client.init call or not injecting dynamically the api.js script.

mesqueeb commented 6 years ago

@TMSCH I found out what the problem is. Right before window.gapi.client.init(gapiConfig) I logged window.gapi.client and it seems it's still undefined at that point.

Is there a better way that you know of to load the script in a Single Page application besides:

window.gapi.load('client:auth2', _ => {
  window.gapi.client.init(gapiConfig)
})

I need to be sure the script is loaded before I do window.gapi.client.init(gapiConfig)

TMSCH commented 6 years ago

@mesqueeb that's the proper way to do it, as I mentioned, there's a bug in the loader currently that can create this issue sometimes. A setTimeout(init, 1) seems to workaround the bug.

mesqueeb commented 6 years ago

@TMSCH Thank you for the advice. However this did not resolve the issue for me. I'm not sure how I can use the GAPI js client with this bug. Also adding the script just to the html at the header didn't work for me.

I saw in the one-tap signin documentation they have something to check if the script properly loaded:

window.onGoogleYoloLoad = (googleyolo) => {
  // The 'googleyolo' object is ready for use.
}

Maybe the GAPI js script could get something similar?

This is my updated code with setTimeout()

function loadAndInitGAPI() {
  return new Promise((resolve, reject) => {
    let script = document.createElement('script')
    script.type = 'text/javascript'
    script.src = 'https://apis.google.com/js/api.js'
    script.onload = e => {
      window.gapi.load('client:auth2', _ => {
        console.log('loaded GAPI')
        function initGAPI(){
          if (!window.gapi || window.gapi.client){ return reject('no window.gapi.client') }
          window.gapi.client.init(gapiConfig)
          .then(_ => {
            console.log('initialised GAPI')
            window.GAPIiniOK = true
            // Global auth listener
            gapi.auth2.getAuthInstance().isSignedIn
              .listen(store.dispatch('gapiAuthListener'))
            // Not required anymore if global auth listener does its job!
            store.dispatch('user/updateAuthStatus')
            .then(_ => {
              resolve()
            })
          }).catch(error => {
            dispatch('authenticationError', {error, note: 'error during gapi initialisation'})
            return reject(error)
          })
        }
        setTimeout(initGAPI, 10)
      })
    }
    document.getElementsByTagName('head')[0].appendChild(script)
  })
}
TMSCH commented 6 years ago

There seems to be a typo:

if (!window.gapi || !window.gapi.client)

it lacks the ! in the test for window.gapi.client.

mesqueeb commented 6 years ago

@TMSCH Thanks for now this solved my problem. I leave it up to you to decide if you want to close this issue or not.

TMSCH commented 6 years ago

Let's keep it open. We have an internal bug opened for this too.

Neseke commented 6 years ago

Did you make any progress with fixing the bug?

milenkovic commented 5 years ago

This is still happening, is there any way to detect library is loaded? Something like @mesqueeb suggested?

window.onGoogleScriptLoad = () => { // Script is ready for use. }

roelofjan-elsinga commented 5 years ago

Since I got the update e-mail from @milenkovic, this is how I fixed it (maybe this will help you too @Neseke): First include the script (I'm not sure this is the right script, but this will work for the one you use):

<script type="text/javascript" src="https://apis.google.com/js/api:client.js?onLoad=onGoogleScriptLoaded"></script>

Then you can use the snippet @mesqueeb and @milenkovic shared:

window.onGoogleScriptLoad = () => {
  console.log('The google script has really loaded, cool!');
}

When you see "The google script has really loaded, cool!" in the console, you know the script has loaded and can safely use gapi or window.gapi.

milenkovic commented 5 years ago

Thanks @roelofjan-elsinga, that's exactly what I needed.

gustavomassa commented 5 years ago

I'm still facing this issue, sometimes it does not load... When gapi is loaded correctly we have this behavior: image

When gapi does not load correctly we have this behavior: image The call to cb=gapi.loaded_1 is missing.

I'm loading the platform.js using this function:

public async loadGoogleApi(): Promise<void> {
        try {
            if (!this.$window.gapi) {
                const libLoaded = this.$q.defer();
                const node = document.createElement('script');
                node.src = 'https://apis.google.com/js/platform.js';
                node.type = 'text/javascript';
                node.onload = libLoaded.resolve;
                node.onerror = libLoaded.reject;
                node.async = true;
                node.charset = 'utf-8';
                document.getElementsByTagName('head')[0].appendChild(node);
                await libLoaded.promise;
            }
            if (!this.$window.gapi) throw new Error('Failed to load google api');

            if (!this.$window.gapi.signin2) {
                const signin2Loaded = this.$q.defer();
                this.$window.gapi.load('signin2', {
                    callback: function () { signin2Loaded.resolve(true) },
                    onerror: function () { signin2Loaded.reject('gapi.signin2 failed to load') },
                    timeout: 5000, // 5 seconds.
                    ontimeout: function () { signin2Loaded.reject('gapi.signin2 load timeout reached') }
                });
                await signin2Loaded.promise;
            }

            if (!this.$window.gapi.auth2) {
                const auth2Loaded = this.$q.defer();
                this.$window.gapi.load('auth2', {
                    callback: function () { auth2Loaded.resolve(true) },
                    onerror: function () { auth2Loaded.reject('gapi.auth2 failed to load') },
                    timeout: 5000, // 5 seconds.
                    ontimeout: function () { auth2Loaded.reject('gapi.auth2 load timeout reached') }
                });
                await auth2Loaded.promise;
            }

        } catch (ex) {
            HandleError.exception(ex);
        }
    }

Even using the onLoad=onGoogleScriptLoaded like the example above, I face the same issue. I've tried loading the script sync/async, same issue for both.

After some debugging, the gapi is always loaded inside the window object, the problem is the google button that sometimes is not rendered.

The work around is to manually render the button like that:

public renderGoogleLoginButton(buttonID: string): void {
        try {
            if (!buttonID) throw new Error('Google buttonID is NULL');

            this.loadGoogleApi().then(() => {
                this.$window.gapi.signin2.render(buttonID,
                    {
                        'scope': 'email profile',
                        //width': 200,
                        //'height': 50,
                        //'longtitle': false,
                        'theme': 'dark',
                        'onsuccess': this.handleGoogleLogin.bind(this),
                        'onfailure': this.handleGoogleError.bind(this)
                    });
            }).catch((ex) => {
                HandleError.exception(ex);
            });

        } catch (ex) {
            HandleError.exception(ex);
        }
    }
hrmoller commented 5 years ago

As @gustavomassa I experience fluctuating behaviour - sometimes the onLoad callback is invoked and sometimes it's not.

As a workaround I've done this which is a complete hack and not acceptable in an app where performance is somewhat a priority but for the project I'm working on here it should be alright for now.

window.gapiWasLoaded = () => {
  if (!gapiWasLoaded) {
    gapiWasLoaded = true;
    ReactDOM.render(<App />, document.getElementById('root'));
  }
};

setTimeout(() => {
  // @ts-ignore
  window.gapiWasLoaded();
}, 1000);
wt commented 4 years ago

I am experiencing the same. I am trying to use gapi from React. Just out of curiosity, is any work going into fixing this?

roelofjan-elsinga commented 4 years ago

I am experiencing the same. I am trying to use gapi from React. Just out of curiosity, is any work going into fixing this?

Have you tried my solution in this post yet? This solved the problem for me. It's not a GAPI problem, but an implementation problem.

wt commented 4 years ago

I've tried many variations on that theme. Here's a link to the current code try and a website preview. Slowing it down with my browser dev tools makes the problem more likely to occur.

wt commented 4 years ago

The problem appears to be that the gapi.client.init doesn't always result in running either the fulfilled or the rejected callbacks provide to the then method. Is there any way to detect that situation? FWIW, this only seems to happen when I increase he latency for my network connection with the dev tools.

wt commented 4 years ago

So, I moved the <script> tags for gapi to the bottom of the <body>, and it seems to be working. I am not sure why that seems to be making a difference, but it does.

Here's a link to the commit: https://github.com/ymatyt/ymatyt_site/commit/f031a628e613494baba30a80e625e84e7f6ebf0f

DiaaEddin commented 4 years ago

I came up with a solution that makes use of both @mesqueeb and @roelofjan-elsinga solutions. It grantees that gapiLoaded is initialized before loading Google Api and it's safe to call gapi inside it.

window.gapiLoaded= function() {
    //you can safely call gapi here
};
(function(d, s, id) {
    var js,
        p = d.getElementsByTagName(s)[0];
    if (d.getElementById(id)) {
        return;
    }
    js = d.createElement(s);
    js.id = id;
    js.src =
        'https://apis.google.com/js/platform.js?onload=gapiLoaded';
    p.parentNode.insertBefore(js, p);
})(document, 'script', 'google-api-js');
efalayi commented 4 years ago

@mesqueeb we observed this issue when injecting the api.js script asynchronously, in some browsers. We're not sure yet what the root cause of the bug is, but in the meantime you can use a setTimeout before the gapi.client.init call or not injecting dynamically the api.js script.

Works. Thanks @TMSCH

If you are like me and need to dispatch an action after gapi is loaded, the snippet below did the trick:

async function loadGAPI() {
  let loaded = false;
  let errorMessage = null;

  const gapiClientLoad = new Promise((resolve, reject) => {
    setTimeout(() => {
      window.gapi.load('client:auth2', async () => {
        try {
          await window.gapi.client.init(GAPI_CONFIG);
          resolve({ errorMessage, loaded: true });
        } catch (gapiErrorResponse) {
          errorMessage = gapiErrorResponse.error.message;
          reject({ error: errorMessage, loaded });
        }
      })
    }, 1000);
  });

  return gapiClientLoad;
}

Calling loadGAPI resolves or rejects a value.

manishatiwari1696 commented 4 years ago

Hello everybody, I'am facing issue with google calendar. The issue is "when user1 is connecting his google calendar with our website account and then logout his account from our website only and then user2 connect his google calendar with his account of our website and then logout. So what happening is, now if user1 is login to his account then he is not able to access his google calendar and the error image link is shown below". (https://user-images.githubusercontent.com/38031527/88327203-730e2b00-cd44-11ea-86d2-23bea41cf789.jpeg) .

Please help me out, what should I need to do to make this process work. I'm stuck with this issue from long time. I must be missing something.

sudipstha08 commented 2 years ago

Facing same issue in nextjs . Is there any solutions yet ?

sudipstha08 commented 2 years ago

Since I got the update e-mail from @milenkovic, this is how I fixed it (maybe this will help you too @Neseke): First include the script (I'm not sure this is the right script, but this will work for the one you use):

<script type="text/javascript" src="https://apis.google.com/js/api:client.js?onLoad=onGoogleScriptLoaded"></script>

Then you can use the snippet @mesqueeb and @milenkovic shared:

window.onGoogleScriptLoad = () => {
  console.log('The google script has really loaded, cool!');
}

When you see "The google script has really loaded, cool!" in the console, you know the script has loaded and can safely use gapi or window.gapi.

This didn't work in nextjs. Any ideas ?

mkolodziej commented 1 year ago

Had the same problem. Solved it by calling gapi.load after loading the script.

My solution (Typescript):

function loadScript(url: string) {
    console.log(`Loading script from ${url} ...`)
    return new Promise<Event>((resolve, reject) => {
        const script = document.createElement("script");
        script.src = url;
        script.async = true;
        script.onload = (ev) => { resolve(ev); };
        script.onerror = reject;
        document.body.appendChild(script);
      }
    );
}

function loadGapiClient() {
  new Promise<void>((resolve, reject) => {
    gapi.load('client', {
      callback: function() {
        // Handle gapi.client initialization.
        console.log('gapi.client loaded successfully!')
        console.log(`gapi.client : ${gapi.client}`);
        resolve();
      },
      onerror: function() {
        // Handle loading error.
        console.log('gapi.client failed to load!');
        reject();
      },
      timeout: 5000, // 5 seconds.
      ontimeout: function() {
        // Handle timeout.
        console.log('gapi.client could not load in a timely manner!');
        reject();
      }
    });
  });
}

await loadScript("https://apis.google.com/js/api.js");
await loadGapiClient();
DoisKoh commented 6 months ago

Had the same problem. Solved it by calling gapi.load after loading the script.

My solution (Typescript):

function loadScript(url: string) {
    console.log(`Loading script from ${url} ...`)
    return new Promise<Event>((resolve, reject) => {
        const script = document.createElement("script");
        script.src = url;
        script.async = true;
        script.onload = (ev) => { resolve(ev); };
        script.onerror = reject;
        document.body.appendChild(script);
      }
    );
}

function loadGapiClient() {
  new Promise<void>((resolve, reject) => {
    gapi.load('client', {
      callback: function() {
        // Handle gapi.client initialization.
        console.log('gapi.client loaded successfully!')
        console.log(`gapi.client : ${gapi.client}`);
        resolve();
      },
      onerror: function() {
        // Handle loading error.
        console.log('gapi.client failed to load!');
        reject();
      },
      timeout: 5000, // 5 seconds.
      ontimeout: function() {
        // Handle timeout.
        console.log('gapi.client could not load in a timely manner!');
        reject();
      }
    });
  });
}

await loadScript("https://apis.google.com/js/api.js");
await loadGapiClient();

I have no idea why this works. I've tried doing this awaiting the new Promises directly... but for some reason it doesn't work and has to wrapped in functions??! I must be making some typos somewhere... what's special about this? The key things I'm looking at is that async is true, and it's being appended to body instead of head...

Al3676 commented 5 months ago

Excellent gaidunce