Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Google Sign-In for Websites and Angular 2 using Typescript

I'm building a site that has a pretty standard RESTful web service to handle persistence and complex business logic. The UI I'm building to consume this service is using Angular 2 with components written in TypeScript.

Rather than build my own authentication system, I'm hoping to rely on Google Sign-In for Websites. The idea being that users will come to the site, sign in via the framework provided there and then send along the resulting ID tokens, which the server hosting the RESTful service can then verify.

In the Google Sign-In documentation there are instructions for creating the login button via JavaScript which is what needs to happen since the login button is being rendered dynamically in an Angular template. The relevant portion of the template:

<div class="login-wrapper">   <p>You need to log in.</p>   <div id="{{googleLoginButtonId}}"></div> </div> <div class="main-application">   <p>Hello, {{userDisplayName}}!</p> </div> 

And the Angular 2 component definition in Typescript:

import {Component} from "angular2/core";  // Google's login API namespace declare var gapi:any;  @Component({     selector: "sous-app",     templateUrl: "templates/sous-app-template.html" }) export class SousAppComponent {   googleLoginButtonId = "google-login-button";   userAuthToken = null;   userDisplayName = "empty";    constructor() {     console.log(this);   }    // Angular hook that allows for interaction with elements inserted by the   // rendering of a view.   ngAfterViewInit() {     // Converts the Google login button stub to an actual button.     api.signin2.render(       this.googleLoginButtonId,       {         "onSuccess": this.onGoogleLoginSuccess,         "scope": "profile",         "theme": "dark"       });   }    // Triggered after a user successfully logs in using the Google external   // login provider.   onGoogleLoginSuccess(loggedInUser) {     this.userAuthToken = loggedInUser.getAuthResponse().id_token;     this.userDisplayName = loggedInUser.getBasicProfile().getName();     console.log(this);   } } 

The basic flow goes:

  1. Angular renders the template and the message "Hello, empty!" is shown.
  2. The ngAfterViewInit hook is fired and the gapi.signin2.render(...) method is called which converts the empty div into a Google login button. This works correctly and clicking on that button will trigger the login process.
  3. This also attaches the component's onGoogleLoginSuccess method to actually process the returned token after a user logs in.
  4. Angular detects that the userDisplayName property has changed and updates the page to now display "Hello, Craig (or whatever your name is)!".

The first problem that occurs is in the onGoogleLoginSuccess method. Notice the console.log(...) calls in the constructor and in that method. As expected, the one in the constructor returns the Angular component. The one in the onGoogleLoginSuccess method, however, returns the JavaScript window object.

So it looks like the context is getting lost in the process of hopping out to Google's login logic so my next step was to try incorporating jQuery's $.proxy call to hang on to the correct context. So I import the jQuery namespace by adding declare var $:any; to the top of the component and then convert the contents of the ngAfterViewInit method to be:

// Angular hook that allows for interaction with elements inserted by the // rendering of a view. ngAfterViewInit() {     var loginProxy = $.proxy(this.onGoogleLoginSuccess, this);      // Converts the Google login button stub to an actual button.     gapi.signin2.render(       this.googleLoginButtonId,       {         "onSuccess": loginProxy,         "scope": "profile",         "theme": "dark"       }); } 

After adding that, the two console.log calls return the same object so property values are now updating correctly. The second log message shows the object with the expected updated property values.

Unfortunately, the Angular template does not get updated when this happens. While debugging, I stumbled on something that I believe explains what is going on. I added the following line to the end of the ngAfterViewInit hook:

setTimeout(function() {   this.googleLoginButtonId = this.googleLoginButtonId },   5000); 

This shouldn't really do anything. It just waits five seconds after the hook ends and then sets a property value equal to itself. However, with the line in place the "Hello, empty!" message turns into "Hello, Craig!" about five seconds after the page has loaded. This suggest to me that Angular just isn't noticing that the property values are changing in the onGoogleLoginSuccess method. So when something else happens to notify Angular that property values have changed (such as the otherwise useless self-assignment above), Angular wakes up and updates everything.

Obviously that's not a hack I want to leave in place so I'm wondering if any Angular experts out there can clue me in? Is there some call I should be making to force Angular to notice some properties have changed?

UPDATED 2016-02-21 to provided clarity on the specific answer that solved the problem

I ended up needing to use both pieces of the suggestion provided in the selected answer.

First, exactly as suggested, I needed to convert the onGoogleLoginSuccess method to use an arrow function. Secondly, I needed to make use of an NgZone object to make sure that the property updates occurred in a context of which Angular is aware. So the final method ended up looking like

onGoogleLoginSuccess = (loggedInUser) => {     this._zone.run(() => {         this.userAuthToken = loggedInUser.getAuthResponse().id_token;         this.userDisplayName = loggedInUser.getBasicProfile().getName();     }); } 

I did need to import the _zone object: import {Component, NgZone} from "angular2/core";

I also needed to inject it as suggested in the answer via the class's contructor: constructor(private _zone: NgZone) { }

like image 538
Craig Phillips Avatar asked Feb 20 '16 23:02

Craig Phillips


People also ask

Does angular 2 use TypeScript?

Angular 2 is built on TypeScript, which uses ES6 syntax and compiles to vanilla JavaScript. Standard ES5 JavaScript is also valid TypeScript, so you can still use your existing code. Where TypeScript differs is in the typing system. Although it is optional, I recommend you use the typing system in your projects.

Does Google support TypeScript?

Google is using TypeScript and Angular on TypeScript for its famous products such as Google Analytics, Firebase, and Google Cloud Platform including its critical internal tools - bug tracking, employee reviews, and product approval and launch tools.

Can TypeScript be used with Angular?

Angular is a modern framework built entirely in TypeScript, and as a result, using TypeScript with Angular provides a seamless experience. The Angular documentation not only supports TypeScript as a first-class citizen, but uses it as its primary language.

Is angular 2 JavaScript or TypeScript?

AngularJS is by far the most popular JavaScript framework available today for creating web applications. And now Angular 2 and TypeScript are bringing true object oriented web development to the mainstream, in a syntax that is strikingly close to Java 8.


Video Answer


2 Answers

For your first problem solution is to use arrow function which will preserve context of this :

  onGoogleLoginSuccess = (loggedInUser) => {     this.userAuthToken = loggedInUser.getAuthResponse().id_token;     this.userDisplayName = loggedInUser.getBasicProfile().getName();     console.log(this);   } 

Second issue is happening because third-party scripts run outside the context of Angular. Angular uses zones so when you run something, for example setTimeout(), which is monkey-patched to run in the zone, Angular will get notified. You would run jQuery in zone like this:

  constructor(private zone: NgZone) {     this.zone.run(() => {       $.proxy(this.onGoogleLoginSuccess, this);     });   } 

There are many questions/answers about the zone with much better explanations then mine, if you want to know more, but it shouldn't be an issue for your example if you use arrow function.

like image 69
Sasxa Avatar answered Oct 07 '22 23:10

Sasxa


I made a google-login component if you want an example.

  ngOnInit()   {     this.initAPI = new Promise(         (resolve) => {           window['onLoadGoogleAPI'] =               () => {                   resolve(window.gapi);           };           this.init();         }     )   }    init(){     let meta = document.createElement('meta');     meta.name = 'google-signin-client_id';     meta.content = 'xxxxx-xxxxxx.apps.googleusercontent.com';     document.getElementsByTagName('head')[0].appendChild(meta);     let node = document.createElement('script');     node.src = 'https://apis.google.com/js/platform.js?onload=onLoadGoogleAPI';     node.type = 'text/javascript';     document.getElementsByTagName('body')[0].appendChild(node);   }    ngAfterViewInit() {     this.initAPI.then(       (gapi) => {         gapi.load('auth2', () =>         {           var auth2 = gapi.auth2.init({             client_id: 'xxxxx-xxxxxx.apps.googleusercontent.com',             cookiepolicy: 'single_host_origin',             scope: 'profile email'           });           auth2.attachClickHandler(document.getElementById('googleSignInButton'), {},               this.onSuccess,               this.onFailure           );         });       }     )   }    onSuccess = (user) => {       this._ngZone.run(           () => {               if(user.getAuthResponse().scope ) {                   //Store the token in the db                   this.socialService.googleLogIn(user.getAuthResponse().id_token)               } else {                 this.loadingService.displayLoadingSpinner(false);               }           }       );   };    onFailure = (error) => {     this.loadingService.displayLoadingSpinner(false);     this.messageService.setDisplayAlert("error", error);     this._ngZone.run(() => {         //display spinner         this.loadingService.displayLoadingSpinner(false);     });   } 

It's a bit late but I just want to give an example if someone want to use google login api with ng2.

like image 23
Fr4NgUs Avatar answered Oct 08 '22 00:10

Fr4NgUs