gapi is not defined - Google sign in issue with gapi.auth2.init - javascript

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>

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
});
});

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.

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
});
}

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();
}
}

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>
Sign out
</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>

This worked for me:
https://stackoverflow.com/a/55314602/1034622
Include this script tag
<script src="https://apis.google.com/js/platform.js"></script>

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

Related

How to implement Gmail API using VueJS

I've recently been reading up on the Gmail API, and i want to implement it onto my web application, howeverf I am struggling to get the source code to work on VueJS.
<!DOCTYPE html>
<html>
<head>
<title>Gmail API Quickstart</title>
<meta charset="utf-8" />
</head>
<body>
<p>Gmail API Quickstart</p>
<!--Add buttons to initiate auth sequence and sign out-->
<button id="authorize_button" style="display: none;">Authorize</button>
<button id="signout_button" style="display: none;">Sign Out</button>
<pre id="content" style="white-space: pre-wrap;"></pre>
<script type="text/javascript">
// Client ID and API key from the Developer Console
var CLIENT_ID = '<YOUR_CLIENT_ID>';
var API_KEY = '<YOUR_API_KEY>';
// Array of API discovery doc URLs for APIs used by the quickstart
var DISCOVERY_DOCS = ["https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest"];
// Authorization scopes required by the API; multiple scopes can be
// included, separated by spaces.
var SCOPES = 'https://www.googleapis.com/auth/gmail.readonly';
var authorizeButton = document.getElementById('authorize_button');
var signoutButton = document.getElementById('signout_button');
/**
* On load, called to load the auth2 library and API client library.
*/
function handleClientLoad() {
gapi.load('client:auth2', initClient);
}
/**
* Initializes the API client library and sets up sign-in state
* listeners.
*/
function initClient() {
gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
}).then(function () {
// Listen for sign-in state changes.
gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
// Handle the initial sign-in state.
updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
authorizeButton.onclick = handleAuthClick;
signoutButton.onclick = handleSignoutClick;
}, function(error) {
appendPre(JSON.stringify(error, null, 2));
});
}
/**
* Called when the signed in status changes, to update the UI
* appropriately. After a sign-in, the API is called.
*/
function updateSigninStatus(isSignedIn) {
if (isSignedIn) {
authorizeButton.style.display = 'none';
signoutButton.style.display = 'block';
listLabels();
} else {
authorizeButton.style.display = 'block';
signoutButton.style.display = 'none';
}
}
/**
* Sign in the user upon button click.
*/
function handleAuthClick(event) {
gapi.auth2.getAuthInstance().signIn();
}
/**
* Sign out the user upon button click.
*/
function handleSignoutClick(event) {
gapi.auth2.getAuthInstance().signOut();
}
/**
* Append a pre element to the body containing the given message
* as its text node. Used to display the results of the API call.
*
* #param {string} message Text to be placed in pre element.
*/
function appendPre(message) {
var pre = document.getElementById('content');
var textContent = document.createTextNode(message + '\n');
pre.appendChild(textContent);
}
/**
* Print all Labels in the authorized user's inbox. If no labels
* are found an appropriate message is printed.
*/
function listLabels() {
gapi.client.gmail.users.labels.list({
'userId': 'me'
}).then(function(response) {
var labels = response.result.labels;
appendPre('Labels:');
if (labels && labels.length > 0) {
for (i = 0; i < labels.length; i++) {
var label = labels[i];
appendPre(label.name)
}
} else {
appendPre('No Labels found.');
}
});
}
</script>
<script async defer src="https://apis.google.com/js/api.js"
onload="this.onload=function(){};handleClientLoad()"
onreadystatechange="if (this.readyState === 'complete') this.onload()">
</script>
</body>
</html>
I would like to convert this code into VueJS or atleast get it to work on my vue application. Would anyone be able to help me convert this?
There's nothing stopping you from setting the script tag programmatically in the mounted() hook, with document.createElement(), setAttribute(), document.appendChild() methods. Then wrap your gapi functions inside a try catch statement or use a promise, inspired by this tutorial (google maps): https://markus.oberlehner.net/blog/using-the-google-maps-api-with-vue/

Failing to export variable defined in function to another concurrently-ran JS file

I have been reading different posts on Google and SO, but I just can't figure out why this does not work.
There are 2 HTML and 2 JS files involved in this case (explanation given below in words after the code chunks).
1) index.html
<div id="center">
<img id="logo" src="../img/logowshadow.png" alt="logo">
<p id="para">Get your google slides!</p>
<!--Add buttons to initiate auth sequence and sign out-->
<button id="authorize-button" style="display: none;">Sign in</button>
<button id="signout-button" style="display: none;">Sign Out</button>
</div>
<script src="../js/homePage.js"></script>
<script async defer src="https://apis.google.com/js/api.js"
onload="this.onload=function(){};handleClientLoad()"
onreadystatechange="if (this.readyState === 'complete') this.onload()">
</script>
2) homePage.js
var apiKey = 'AIzaSyCSjg3rrx6Obl4ngZsDlFlV4degUJSMvbw';
var discoveryDocs = ["https://slides.googleapis.com/$discovery/rest?version=v1"];
var clientId = '408869653199-ruoft30vmoqrgpku3us3qd2leb3k6tp1.apps.googleusercontent.com';
var scopes = 'https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/drive';
var authorizeButton = document.getElementById('authorize-button');
var signoutButton = document.getElementById('signout-button');
var user;
var authResponse;
var oauthToken;
var pickerApiLoaded = false;
var chosenPresentation = null;
function handleClientLoad() {
// Load the API client and auth2 library
gapi.load('client:auth2', initClient);
//Load the Picker API
gapi.load('picker', onPickerApiLoad);
}
function initClient() {
gapi.auth2.init({
apiKey: apiKey,
discoveryDocs: discoveryDocs,
clientId: clientId,
scope: scopes
}).then(function () {
// Listen for sign-in state changes.
gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
// Set the current Google User
gapi.auth2.getAuthInstance().currentUser.listen(updateUser);
// Handle the initial sign-in state.
updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
authorizeButton.onclick = handleAuthClick;
signoutButton.onclick = handleSignoutClick;
});
}
// Callback to make sure that the Picker API has loaded
function onPickerApiLoad() {
pickerApiLoaded = true;
createPicker();
}
// Store the current Google user
function updateUser(gUser) {
user = gUser;
updateToken();
}
// Store the access token
function updateToken() {
authResponse = user.getAuthResponse(true);
oauthToken = authResponse.access_token;
}
function updateSigninStatus(isSignedIn) {
if (isSignedIn) {
authorizeButton.style.display = 'none';
signoutButton.style.display = 'block';
createPicker();
} else {
authorizeButton.style.display = 'block';
signoutButton.style.display = 'none';
}
}
function handleAuthClick(event) {
gapi.auth2.getAuthInstance().signIn();
}
function handleSignoutClick(event) {
gapi.auth2.getAuthInstance().signOut();
}
// Create and render a Picker object for picking user slides
function createPicker() {
if (pickerApiLoaded && oauthToken) {
var picker = new google.picker.PickerBuilder().
addView(google.picker.ViewId.PRESENTATIONS).
setOAuthToken(oauthToken).
setDeveloperKey(apiKey).
setCallback(pickerCallback).
build();
picker.setVisible(true);
}
}
// Callback implementation
function pickerCallback(data) {
var url = 'nothing';
if(data[google.picker.Response.ACTION] == google.picker.Action.PICKED) {
var doc = data[google.picker.Response.DOCUMENTS][0];
url = doc[google.picker.Document.URL].replace('edit', 'present');
var item_name = doc[google.picker.Document.NAME];
alert('You picked ' + item_name);
//export the chosen presentation for use in mobileControl.js
chosenPresentation = doc[google.picker.Document.ID];
alert("chosen: " + chosenPresentation);
exports.chosenPresentation = chosenPresentation;
window.location.replace(url);
}
}
3) mobile.html
<div id="instructions">
<p>Swipe <b>left</b> to go to the previous slide.</p>
<p>Swipe <b>right</b> to go to the next slide.</p>
</div>
<script src="../js/mobileControl.js"></script>
and 4) mobileControl.js
alert("Loaded the JavaScript!"); //Shows up
var m = require('./homePage.js');
alert("imported"); //Does not show up
alert(m.chosenPresentation); //Does not show up
To put in words what my code is trying to achieve: users will be able to sign in to their google accounts, and select a set of Presentation found on their Google drive. I want to pass the Presentation ID on from homePage.js to mobileControl.js, and I attempted to do so using Node's exports. I suspect that it is not working because both scripts are run 'at the same time' (index.html is meant to run on the computer, while mobile.html runs on a mobile device...concurrently). But I am not sure if I am right in identifying the cause, and if so, is there a way to export the variable from within the function after it has been defined? Perhaps I should detect it when the slide has been selected, and only load mobilePage.js after everything in homePage.js has ran?
My apologies for the long read, but my previous attempt at diluting the example obviously failed miserably so...
Update: user #vsenko is totally right in saying that I have mixed up client-side programming and server-side programming, so do read up more on this if you are facing the same issue as me
As far as I can see, you are trying to load .js files with NodeJS specific API (RequireJS API to precise) directly on the web page. This is not going to work because browsers do not implement it natively. To utilize this API you will have to use a preprocessor (Webpack, Browserify or something else).
Other apparent problem with your approach is that you assume that it is possible to transfer data between different devices using something that look like NodeJS modules. But it is not possible, you will have transfer data over the network between your devices directly or though your server.

I want to display an alert message status if I lose an internet connection

Using AngularJS, how can I display an alert message status if I lose an internet connection in my app?
I am working on Angularjs Express framework and APIs.
while working on app if I lose internet connection I need to pop up an alert and I should stop calling HTTP requests until I receive a proper internet connection and resume the work.
I even tried with offline.js am not able to solve please if any simple and easy method is there let me know thank you. In header I added all these
<script src="/javascripts/plugins/offlinechecking/offline.min.js"></script>
<link rel="stylesheet" href="/stylesheets/themes/offline-theme-chrome.css" />
<link rel="stylesheet" href="/stylesheets/themes/offline-language-english.css" />
<script>
Offline.options = {
checkOnLoad: false,
interceptRequests: true,
reconnect: {
initialDelay: 3,
delay: (1.5 * last delay, capped at 1 hour)
},
requests: true,
game: false
</script>
You can use online and offline events:
app.run(function($window, $rootScope) {
$rootScope.online = navigator.onLine;
$window.addEventListener("offline", function () {
$rootScope.$apply(function() {
$rootScope.online = false;
});
}, false);
$window.addEventListener("online", function () {
$rootScope.$apply(function() {
$rootScope.online = true;
});
}, false);
});
More info: How to check internet connection in AngularJs
Moreover, you didn't close your Offline.options object:
Offline.options = {
// No closing }.
Your options:
addEventListener on the window, document, or document.body.
setting the .ononline or .onoffline properties on document or
document.body to a JavaScript Function object.
specifying ononline="..." or onoffline="..." attributes on the
tag in the HTML markup
I will demonstrate the easiest.
In you controller
document.body.onoffline = function() {
alert('You are offline now');
$scope.connection = 'offline'
}
document.body.ononline = function() {
alert('You are online again');
$scope.connection = 'online'
}
Check $scope.connection variable before you try to send requests around.
Update for Comment1
If you need the variable to be global, set it in a factory
myApp.factory('connectionStatus', function(){
var connection;
return connection;
})
Now inject this factory wherever you need to use it. This is how it goes inside your main controller where you set the events.
myApp.controller('mainController', function($scope, connectionStatus) {
document.body.onoffline = function() {
alert('You are offline now');
connectionStatus = 'offline'
}
})

Google picker auth popup is being blocked

I have a mobile site which lists jobs, the user applies and uploads their CV (resume) - I want them to be able to choose a file from their Google Drive.
I've created the Hello world example here - https://developers.google.com/picker/docs/ (code reproduced here for convenience)
Problem is that if not already logged into Drive, a popup to login is launched. This is bad enough on a desktop but really bad on a phone.
I have tried this solution, but get 'TypeError: gapi.auth is undefined'
I also tried launching the picker from an onclick event rather than the onload as described by the docs.
function launchDrive()
{
gapi.load('auth', {'callback': onAuthApiLoad});
gapi.load('picker', {'callback': onPickerApiLoad});
}
<input type='button' value='Launch Drive' onclick='launchDrive();'>
Sample Google code:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<title>Google Picker Example</title>
<script type="text/javascript">
var developerKey = 'xxxxxxxYYYYYYYY-12345678';
var clientId = "1234567890-abcdefghijklmnopqrstuvwxyz.apps.googleusercontent.com"
var scope = ['https://www.googleapis.com/auth/photos'];
var pickerApiLoaded = false;
var oauthToken;
function onApiLoad() {
gapi.load('auth', {'callback': onAuthApiLoad});
gapi.load('picker', {'callback': onPickerApiLoad});
}
function onAuthApiLoad() {
window.gapi.auth.authorize(
{
'client_id': clientId,
'scope': scope,
'immediate': false
},
handleAuthResult);
}
function onPickerApiLoad() {
pickerApiLoaded = true;
createPicker();
}
function handleAuthResult(authResult) {
if (authResult && !authResult.error) {
oauthToken = authResult.access_token;
createPicker();
}
}
// Create and render a Picker object for picking user Photos.
function createPicker() {
if (pickerApiLoaded && oauthToken) {
var picker = new google.picker.PickerBuilder().
addView(google.picker.ViewId.PHOTOS).
setOAuthToken(oauthToken).
setDeveloperKey(developerKey).
setCallback(pickerCallback).
build();
picker.setVisible(true);
}
}
// A simple callback implementation.
function pickerCallback(data) {
var url = 'nothing';
if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) {
var doc = data[google.picker.Response.DOCUMENTS][0];
url = doc[google.picker.Document.URL];
}
var message = 'You picked: ' + url;
document.getElementById('result').innerHTML = message;
}
</script>
</head>
<body>
<div id="result"></div>
<!-- The Google API Loader script. -->
<script type="text/javascript" src="https://apis.google.com/js/api.js?onload=onApiLoad"></script>
</body>
</html>
13 May 2015 edit
Further to Jason's answer, here is what I also tried (called by a button oncllick):
function launchDrive()
{
//gapi.load('auth', {'callback': onAuthApiLoad});
gapi.auth.init(onAuthApiLoad);
gapi.load('picker', {'callback': onPickerApiLoad});
}
You will want to call gapi.auth.init. See the docs here: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauthinit
Initializes the authorization feature. Call this when the client loads to prevent popup blockers from blocking the auth window on gapi.auth.authorize calls.
To solve your issue you need to understand how google performs oauth in your case:
gapi performs init actions
gapi opens a google auth page in new popup window and you perform login in it
after login succeeded gapi gets notified and you receive your token
Why browser blocks the popup in 2nd step:
original event is not present anymore in window (window.event is destroyed).
User manually blocked the popup from current site
So if user didn't block a popup and you popup is still blocked, gapi actions look something like:
<input type="button" onclick="auth" value="click"/>
function auth() {
setTimeout(function() {
// by this time window.event is destroyed, that's why browser blocks the popup
window.open(document.URL, '_blank', 'location=yes,height=570,width=520,scrollbars=yes,status=yes');
}, 100)
}
So what you should do:
Make sure that after button click you don't perform any asynchronous actions like XHRequests or other
Make sure gapi is inited and ready by the time users click on the button, so by the time gapi needs to create a popup, window.event won't be null. So move all gapi init methods to DOMContentLoaded.
As another option you could use server-side oauth flow, which means instead of popup user will be redirected in current tab to gauth page.
I have it working now.
In the example for the picker, https://developers.google.com/picker/docs/, it calls:
<script type="text/javascript" src="https://apis.google.com/js/api.js?onload=onApiLoad"></script>
In this example, https://developers.google.com/api-client-library/javascript/start/start-js, it calls:
<script src="https://apis.google.com/js/client.js?onload=handleClientLoad"></script>
Using client.js fixes the 'TypeError: gapi.auth is undefined' issue, and thus the login popup works.
Maybe api.js is an older version of the API?
Just Skip to the bottom
Here's the code that works for me currently. This is my first hour using this API though, so I really don't know what any of these functions do yet, nor do I know what the proper order and error handling is just yet, but at least this is functional now. Maybe it'll help someone else in the future.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<title>Google Picker Example</title>
</head>
<body style="width: 70%; margin: 100px auto;">
<!-- Added a button to open picker -->
<button onclick="loadPicker();" >Open from GoogleDrive</button>
<div id="result"></div>
<!-- Moved to end of body tag instead of head -->
<script type="text/javascript">
// The Browser API key obtained from the Google API Console.
// Replace with your own Browser API key, or your own key.
var developerKey = '<IDK WHAT'S SUPPOSED TO GO HERE, BUT ITS OK>';
// The Client ID obtained from the Google API Console. Replace with your own Client ID.
var clientId = "<YOUR CLIENT ID GOES HERE>.apps.googleusercontent.com"
// Replace with your own project number from console.developers.google.com.
// See "Project number" under "IAM & Admin" > "Settings"
var appId = "<YOUR APP ID GOES HERE>";
// Scope to use to access user's Drive items.
var scope = ['https://www.googleapis.com/auth/drive'];
var pickerApiLoaded = false;
var oauthToken;
// Use the Google API Loader script to load the google.picker script.
function loadPicker() {
// This needs to be client:auth2 no client
gapi.load('client:auth2', {'callback': onAuthApiLoad});
gapi.load('picker', {'callback': onPickerApiLoad});
}
function onAuthApiLoad() {
// we need to init gapi.client with the clientId and scope first
gapi.client.init({
clientId: clientId,
scope: scope
});
// Now we can authorize? seems like the same thing here
window.gapi.auth.authorize(
{
'client_id': clientId,
'scope': scope,
'immediate': false
},
handleAuthResult);
}
function onPickerApiLoad() {
pickerApiLoaded = true;
createPicker();
}
function handleAuthResult(authResult) {
if (authResult && !authResult.error) {
oauthToken = authResult.access_token;
createPicker();
}
}
// Create and render a Picker object for searching images.
function createPicker() {
// Wow this is a mess
if (pickerApiLoaded && oauthToken) {
var view = new google.picker.View(google.picker.ViewId.DOCS);
view.setMimeTypes("image/png,image/jpeg,image/jpg");
var picker = new google.picker.PickerBuilder()
.enableFeature(google.picker.Feature.NAV_HIDDEN)
.enableFeature(google.picker.Feature.MULTISELECT_ENABLED)
.setAppId(appId)
.setOAuthToken(oauthToken)
.addView(view)
.addView(new google.picker.DocsUploadView())
// Guess this is... optional?
//.setDeveloperKey(developerKey)
.setCallback(pickerCallback)
.build();
picker.setVisible(true);
}
}
// A simple callback implementation.
function pickerCallback(data) {
if (data.action == google.picker.Action.PICKED) {
var fileId = data.docs[0].id;
alert('Selected fileId: ' + fileId);
}
}
</script>
<!-- The Google API Loader script. Removed the autorun -->
<script type="text/javascript" src="https://apis.google.com/js/api.js"></script>
</body>
</html>
Edit: If you get a pop-up window that doesn't load, just close it and click the button again. That fixed another issue I just had.
Again, I don't know what I'm doing yet, so hopefully I can get a better understanding of this and clarify things later.
E2: Ah, there's more information about OAuth2 over on the Javascript GAPI documentation page which can be found here: https://developers.google.com/api-client-library/javascript/features/authentication
From another document, it appear that gapi.load('client', callback) will load auth2 if not already loaded. Calling gapi.load('client:auth2', callback) will just save 1 network request.
Note: when you authorize your application using Oauth 2.0, you do not also need to set the API key as in the first example. However, it is a good practice to do so, in case your code ever expands to handle unauthorized requests.
That explains why I could remove the API/developer key.
Edit 3: Ok the above code is technically wrong.
Warning: do not use this method alongside the recommended gapi.auth2.init and signIn flow. These are two distinct behaviors (Authorization for gapi.auth2.authorize vs Authentication for gapi.auth2.init/signIn) and will have unexpected issues if used within the same application.
autorize is for single use authentications (if you were logged into 2 google accounts for instance). While using gapi.init() is meant to be for a more long term session (like for logging in and out of a website).
How this is working currently, I do not know.
Don't use the above code, just wanted to document the progress. Here's a better demo working with getAuthResponse()
<html>
<head></head>
<body>
<div style="padding: 50px">
<h2 style="color: #2196f3;">Status: <span id='status'></span></h2>
<button id="signin-button" onclick="handleSignInClick()">Sign In</button>
<button id="signout-button" onclick="handleSignOutClick()">Sign Out</button>
<button id="signout-button" onclick="openFile()">Open File</button>
</div>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script type="text/javascript">
var cid = '<CLIENTID_HERE>';
var scope = 'https://www.googleapis.com/auth/drive';
var authenticated = false;
var pickerLoaded = false;
var auth = null;
var user = null;
var response = null;
var token = null;
var stat = $('#status');
function openFile() {
gapi.load('client:auth2', initClient);
gapi.load('picker', onPickerLoad);
}
function initClient() {
stat.html("starting");
gapi.client.init({
clientId: cid,
scope: scope
}).then(
function () {
console.log("init");
// Check if we are logged in.
auth = gapi.auth2.getAuthInstance();
auth.isSignedIn.listen(onStatusChange);
authenticated = auth.isSignedIn.get();
stat.html(authenticated);
if (authenticated) {
stat.html("Logged In!");
user = auth.currentUser.get();
response = user.getAuthResponse(true);
token = response.access_token;
showPicker();
} else {
stat.html("Logged Out!");
}
}, function(){stat.html("error")});
}
function onStatusChange(isSignedIn) {
if (isSignedIn) {
stat.html("Logged In!");
authenticated = true;
user = auth.currentUser.get();
response = user.getAuthResponse(true);
token = response.access_token;
showPicker();
showPicker();
} else {
authenticated = false;
stat.html("Logged Out!");
}
}
function handleSignInClick(event) {
gapi.auth2.getAuthInstance().signIn();
}
function handleSignOutClick(event) {
gapi.auth2.getAuthInstance().signOut();
alert("signed out");
}
function onPickerLoad() {
pickerLoaded = true;
showPicker();
}
function showPicker() {
if (pickerLoaded && authenticated) {
var view = new google.picker.View(google.picker.ViewId.DOCS);
var picker = new google.picker.PickerBuilder();
picker.addView(view);
picker.enableFeature(google.picker.Feature.MULTISELECT_ENABLED);
picker.setOAuthToken(token);
picker.setAppId()
picker.setCallback(onDriveFileOpen);
picker = picker.build();
picker.setVisible(true);
}
}
function onDriveFileOpen(data) {
console.log(data);
if (data.action == google.picker.Action.PICKED) {
var fileId = data.docs[0].id;
console.log(fileId);
alert(data.docs[0].name);
}
}
</script>
<script async defer src="https://apis.google.com/js/api.js">
</script>
</body>
</html>

Google OAuth2 and app script in IFRAME sandbox

I am a newbie as far as web development is concerned and even more so with Google App Scripts and OAuth2.0. Having said that, I have researched enough and also tried several tricks, but still can't get past this issue.
I borrowed sample from here:
Google Developers - Client API Library
Then created an Apps Script project with an index.html file with code from that page. I also created a project on the developer console, created a client ID, API key and turned on the required API support. I also made the required changes to the sample to reflect the new client ID and API key.
The index.html page is served from HTML Service with SandBox Mode set to IFRAME. If I load the URL in a browser window (say using incognito mode) and click "Authorize" button, it opens the Google sign-in window. But after signing in, it opens two new tabs with messages
Please close this window
and the original browser window shows no change.
The JavaScript console shows error messages like these:
Unsafe JavaScript attempt to initiate navigation for frame with URL ''
from frame with URL
https://accounts.google.com/o/oauth2/postmessageRelay?parent=https%3A%2F%2F…6lxdpyio6iqy-script.googleusercontent.com#rpctoken=288384029&forcesecure=1.
The frame attempting navigation is sandboxed, and is therefore
disallowed from navigating its ancestors.
From the messages, it seems its an effect of using IFRAME and some sort of security feature is preventing the callback being delivered to the original window. If I reload the original window, things work OK. But that's not what I would ideally like.
How do I work around this issue? Its a very simple project and I can provide source code if that helps.
Thanks,
Pavan
Edit: Here is the sample code I'm trying. You would need to have your client ID and API Key and also set JS origins in the Google Console for things to work:
Code.gs
function doGet(e) {
return HtmlService.createHtmlOutputFromFile('index').setSandboxMode(HtmlService.SandboxMode.IFRAME);
}
index.html
<!--
Copyright (c) 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License.
To run this sample, replace YOUR API KEY with your application's API key.
It can be found at https://code.google.com/apis/console/?api=plus under API Access.
Activate the Google+ service at https://code.google.com/apis/console/ under Services
-->
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
</head>
<body>
<!--Add a button for the user to click to initiate auth sequence -->
<button id="authorize-button" style="visibility: hidden">Authorize</button>
<script type="text/javascript">
// Enter a client ID for a web application from the Google Developer Console.
// The provided clientId will only work if the sample is run directly from
// https://google-api-javascript-client.googlecode.com/hg/samples/authSample.html
// In your Developer Console project, add a JavaScript origin that corresponds to the domain
// where you will be running the script.
var clientId = 'YOUR_CLIENT_ID';
// Enter the API key from the Google Develoepr Console - to handle any unauthenticated
// requests in the code.
// The provided key works for this sample only when run from
// https://google-api-javascript-client.googlecode.com/hg/samples/authSample.html
// To use in your own application, replace this API key with your own.
var apiKey = 'YOUR API KEY';
// To enter one or more authentication scopes, refer to the documentation for the API.
var scopes = 'https://www.googleapis.com/auth/plus.me';
// Use a button to handle authentication the first time.
function handleClientLoad() {
gapi.client.setApiKey(apiKey);
window.setTimeout(checkAuth,1);
}
function checkAuth() {
gapi.auth.authorize({client_id: clientId, scope: scopes, immediate: true, response_type: 'token'}, handleAuthResult);
}
function handleAuthResult(authResult) {
var authorizeButton = document.getElementById('authorize-button');
if (authResult && !authResult.error) {
authorizeButton.style.visibility = 'hidden';
makeApiCall();
} else {
authorizeButton.style.visibility = '';
authorizeButton.onclick = handleAuthClick;
}
}
function handleAuthClick(event) {
gapi.auth.authorize({client_id: clientId, scope: scopes, immediate: false, response_type: 'token'}, handleAuthResult);
return false;
}
// Load the API and make an API call. Display the results on the screen.
function makeApiCall() {
gapi.client.load('plus', 'v1', function() {
var request = gapi.client.plus.people.get({
'userId': 'me'
});
request.execute(function(resp) {
var heading = document.createElement('h4');
var image = document.createElement('img');
image.src = resp.image.url;
heading.appendChild(image);
heading.appendChild(document.createTextNode(resp.displayName));
heading.appendChild(document.createTextNode(resp.emails[0].value));
document.getElementById('content').appendChild(heading);
});
});
}
</script>
<script src="https://apis.google.com/js/client.js?onload=handleClientLoad"></script>
<div id="content"></div>
<p>Retrieves your profile name using the Google Plus API.</p>
</body>
</html>
found a solution... not nice but works oO:
the trick is to remove the oauth2relay iframes before the auth window is closed. after the window is closed you have to add the frames again and do a immediate request, if that works the user authorized the app.
be careful:
this script does not check if the user meanwhile is logged out or the token is expired, as long as the webapp window is open the same token is used.
Code.js:
function doGet(e) {
return HtmlService.createTemplateFromFile('Index').evaluate().setTitle(formSettings.title).setSandboxMode(HtmlService.SandboxMode.IFRAME);
}
function include(file) {
return HtmlService.createHtmlOutputFromFile(file).getContent();
}
function doPost(meta) {
if (!meta || !meta.auth) {
throw new Error('not authorized');
return;
}
var auth = JSON.parse(UrlFetchApp.fetch('https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=' + meta.auth.access_token, { muteHttpExceptions: true }).getContentText());
if (auth.error || !auth.email) {
throw new Error('not authorized');
return;
}
if (typeof this[meta.method + '_'] == 'function') {
return this[meta.method + '_'](auth.email, meta.data);
}
throw new Error('unknown method');
}
function test_(email, data) {
return email;
}
Index.html:
<html>
<head>
<?!= include('JavaScript'); ?>
</head>
<body>
<div class="content-wrapper">
</div>
</body>
</html>
Javascript.html:
<script type='text/javascript' src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script type='text/javascript' src="//apis.google.com/js/client.js?onload=apiLoaded" async></script>
<script type='text/javascript'>
var clientId = '*************-********************************.apps.googleusercontent.com';
var scopes = ['https://www.googleapis.com/auth/plus.me', 'https://www.googleapis.com/auth/userinfo.email'];
var loaded = false;
var auth = null;
function apiLoaded() {
loaded = true;
login();
}
window._open = window.open;
window._windows = [];
window.open = function(url) {
var w = window._open.apply(window,arguments);
window._windows.push(w);
return w;
}
function login(step) {
step || (step = 0);
if (!loaded) {
return;
}
gapi.auth.authorize({client_id: clientId, scope: scopes, immediate: (step <= 0 || step >= 2) }, function(authResult) {
if (authResult) {
if (authResult.error) {
if (authResult.error == 'immediate_failed' && authResult.error_subtype == 'access_denied' && step <= 0) {
var interval = setInterval(function() {
var $ifr = $('iframe');//[id^=oauth2relay]');
if (!window._windows.length) {
clearInterval(interval);
return;
}
if ($ifr.length) {
clearInterval(interval);
$ifr.detach();
var w = window._windows.pop();
if (w) {
var interval2 = setInterval(function() {
if (w.closed) {
clearInterval(interval2);
$('body').append($ifr);
login(2);
}
});
} else {
$('body').append($ifr);
}
}
},500);
login(1);
} else if (authResult.error == 'immediate_failed' && authResult.error_subtype == 'access_denied' && step >= 2) {
//user canceled auth
} else {
//error
}
} else {
auth = authResult;
doPost('test', { some: 'data' }, 'test');
}
} else {
//error
}
});
}
function test() {
console.log(arguments);
}
//with this method you can do a post request to webapp server
function doPost(method, data, callbackName) {
data || (data = {});
google.script.run.withSuccessHandler(onSuccess).withFailureHandler(onError).withUserObject({ callback: callbackName }).doPost({ method: method, data: data, auth: auth });
}
function onSuccess(data, meta) {
if (typeof window[meta.callback] == 'function') {
window[meta.callback](null, data);
}
}
function onError(err, meta) {
if (typeof window[meta.callback] == 'function') {
window[meta.callback](err);
}
}
</script>

Categories

Resources