Using Typescript with Angular 2 to Implement Google Sign-In on Websites

Currently, I am in the process of creating a website that utilizes a typical RESTful web service to manage persistence and intricate business logic. To consume this service, I am using Angular 2 with components coded in TypeScript.

Instead of developing my own authentication system, I am planning to use Google Sign-In for Websites. The plan is for users to visit the site, sign in through the provided framework, and then send the resulting ID tokens to the server hosting the RESTful service for verification.

The Google Sign-In documentation contains detailed instructions on creating the login button using JavaScript, as the login button will be dynamically rendered in an Angular template. Here is the relevant part 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>

Furthermore, the Angular 2 component definition in Typescript is as follows:

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 allowing 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 unfolds as follows:

  1. Angular renders the template, displaying the message "Hello, empty!"
  2. The ngAfterViewInit hook is executed, calling the gapi.signin2.render(...) method, which transforms the empty div into a Google login button. This works correctly, and clicking the button initiates the login process.
  3. The component's onGoogleLoginSuccess method is also linked to process the returned token post user login.
  4. Angular detects the change in the userDisplayName property and updates the page to show "Hello, Craig (or any other name)!"

The first issue arises in the onGoogleLoginSuccess method, where the context gets lost, indicating the console.log(...) in the constructor returns the Angular component, while the one in the onGoogleLoginSuccess method returns the JavaScript window object.

It appears that the context is lost during the transition to Google's login logic. To address this, I attempted to integrate jQuery's $.proxy call to maintain the correct context. This involved adding declare var $:any; at the top of the component and modifying the contents of the ngAfterViewInit method as follows:

// Angular hook enabling interaction with elements inserted by the view rendering.
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"
      });
}

Following this adjustment, both console.log calls return the same object, ensuring the property values update correctly. The second log displays the object with the expected updated property values.

However, the Angular template does not update accordingly. Through debugging, I discovered adding the following line at the end of the ngAfterViewInit hook:

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

Although seemingly redundant, this line causes the "Hello, empty!" message to transition to "Hello, Craig!" approximately five seconds after the page loads. This implies that Angular fails to notice property value changes in the onGoogleLoginSuccess method, and only reacts when prompted by changes detected through other means.

This makeshift approach is not sustainable, prompting me to seek advice from Angular experts on how to address this. Is there a specific call I should be making to ensure Angular recognizes property changes?

UPDATED 2016-02-21 to provide clarity on the specific solution that resolved the issue

In the end, I found it necessary to implement both aspects of the suggested solution.

Firstly, as recommended, I converted the onGoogleLoginSuccess method to utilize an arrow function. Additionally, I utilized an NgZone object to ensure property updates occur within a context recognized by Angular. The final method now appears as follows:

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

It was necessary to import the _zone object:

import {Component, NgZone} from "angular2/core";

Additionally, I had to inject it as suggested in the response through the class's constructor:

constructor(private _zone: NgZone) { }

Answer №1

If you're facing a problem, one solution is to utilize arrow functions, which help maintain the context of this:

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

The second issue arises because third-party scripts are executed outside the Angular context. Angular operates with zones, so tasks like setTimeout(), when patched to run in the zone, notify Angular. To run jQuery within the zone:

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

There are plenty of resources discussing zones in more depth. However, if you stick to arrow functions, it shouldn't be a problem in your scenario.

Answer №2

If you're looking for a sample, I've created a google-login component that you can reference.

  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 getting a bit late, but I thought I'd share this example for anyone looking to integrate Google login API with ng2.

Answer №3

Add the following code snippet to your main HTML file:

<script src="https://apis.google.com/js/platform.js" async defer></script>

For the login functionality, create a file called login.html and include the code below:

<button id="glogin">Click here to log in with Google</button>

Next, in a file named login.ts, add the following TypeScript code:

declare const gapi: any;
public auth2: any;

ngAfterViewInit() {
     gapi.load('auth2',  () => {
      this.auth2 = gapi.auth2.init({
        client_id: 'YOUR_CLIENT_ID_HERE',
        cookiepolicy: 'single_host_origin',
        scope: 'profile email'
      });
      this.attachSignin(document.getElementById('glogin'));
    });
}

public attachSignin(element) {
    this.auth2.attachClickHandler(element, {},
      (loggedInUser) => {  
      console.log(loggedInUser);

      }, function (error) {
        // alert(JSON.stringify(error, undefined, 2));
      });

 }

Answer №4

Give this package a try -

npm install angular2-google-login

Check it out on Github - https://github.com/rudrakshpathak/angular2-google-login

I have integrated Google login into Angular2 with this package. Just install it and you're good to go.

Here are the steps to follow:

import { AuthService, AppGlobals } from 'angular2-google-login';

Provide providers - providers: [AuthService];

Declare in the constructor -

constructor(private _googleAuth: AuthService){}

Set the Google client ID -

AppGlobals.GOOGLE_CLIENT_ID = 'YOUR_CLIENT_ID_HERE';

Use this to access the service -

this._googleAuth.authenticateUser(()=>{
  //YOUR_CODE_HERE 
});

For logging out -

this._googleAuth.userLogout(()=>{
  //YOUR_CODE_HERE 
});

Answer №5

After receiving some assistance from Sasxa, I discovered a way to bind this to the onSuccess function without using a fat arrow. By utilizing .bind(this), I was able to streamline my code and avoid creating unnecessary functions.

ngAfterViewInit() {
  var loginProxy = $.proxy(this.onGoogleLoginSuccess, this);

  // Converting the Google login button stub to an actual button.
  gapi.signin2.render(
    this.googleLoginButtonId,
    {
      "onSuccess": loginProxy.bind(this),
      "scope": "profile",
      "theme": "dark"
    });
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Difficulty in showcasing error on the website with JavaScript

I have recently developed a webpage that includes several input boxes and a submit button within a form as shown below: <form action="" method="post" name="password"> Upon clicking the submit button, a JavaScript function is triggered to validate i ...

What is the process for dynamically altering the method in a class?

Is there a way to dynamically modify the changeThis method by substituting it with the value of a radio element? For example, if I select event1, the method should be switched to Class.event1() <input type="radio" class="radio-input" ...

The error message "Create controller with service — Get... is not a function" indicates that

Within my ASP.NET Boilerplate project, the following code snippet is present: (function () { appModule.controller('augustinum.views.kostenstelle.index', [ '$scope', '$uibModal', 'abp.services.app.kostenstelle ...

Utilizing @Material-UI Tabs for a Stylish Navigation Bar

As a newcomer to the coding realm, I find myself on a quest for an answer that has proven elusive. Despite scouring various sources, none of the solutions I've stumbled upon seem to work in my case. The issue at hand revolves around my desire to util ...

Creating an Angular table row that can expand and collapse using ng-bootstrap components is a convenient and

I need assistance with an application I am developing, where I want to expand a table row to display details when it is clicked. The issue I am facing is that currently, all rows expand and show the data of the clicked row as seen in the image result below ...

What is the purpose of using square brackets in the angular.module() function in AngularJS?

const myapp=angular.module('myApp',[]); As someone venturing into the realm of angularjs, I have a question. What is the significance of using [] in angular.module()? If anyone could shed some light on this, it would be greatly appreciated. ...

My loop encountered an 'Undefined' error while attempting to retrieve an object from a JSON file

Hey everyone, I could use some help with a problem I'm having in my for loop. The issue is that I keep getting an "undefined" word in the output of my loop. I am trying to retrieve objects from a JSON file and the loop itself seems to be working corre ...

Verify if a personalized angular directive is able to display or conceal an element

I have a custom directive that toggles the visibility of an element based on the value of another service. I attempted to write a test to verify if the directive functions as intended. Initially, I used the ":visible" selector in my test, but it consistent ...

import an external JavaScript file in an HTML document

Looking to load a .js file from a simple HTML file? If you have these files in the same folder: start.js var http = require('http'); var fs = require('fs'); http.createServer(function (req, response) { fs.readFile('index.htm ...

Adjust the sidebar size in Bootstrap 5 to align perfectly with the navbar brand

https://i.stack.imgur.com/Ra0c7.png I'm trying to ensure that the width of the sidebar on this page aligns consistently with the end of the logo in the top right. However, using variations of col-xxl-1 etc. is causing inconsistent sizes at different ...

The requested property does not exist within the 'CommonStore' type in mobx-react library

I'm diving into the world of TypeScript and I'm running into an issue when trying to call a function from another class. I keep getting this error - could it be that functions can only be accessed through @inject rather than import? What am I mis ...

"Error: The angularjs code is unable to access the $http variable within

$http({ url: "php/load.php", method: "GET", params: {'userId':userId} }).success(function(data, status, headers, config) { $scope.mydata = data; mydata = data; }).error(function(data, status, headers, config) { }); It's puzzling why ...

Prioritize moving the JavaScript onload handler to the end of the queue

I am looking to include a JavaScript file at the very end of footer.php using a WordPress plugin. I attempted to do this with the 'add_action' Hook, but found that it is adding the JavaScript code near the </body> tag. add_action('wp_ ...

Failure to fetch data through Axios Post method with a Parameter Object

I've encountered an issue with Axios while attempting to make a post request with a parameters object to a Laravel route. If I use query parameters like ?username=user, the post request works successfully. However, when I use an object, it fails: Be ...

"Exploring the process of assigning input data to a different variable within a Vue component

Reviewing the code snippet I currently have: <template> <div> <input v-model.number="money"> <p>{{ money }}</p> </div> </template> <script> name: 'MyComponent', data () { ...

Hidden warning to React-select for being uncontrolled

I've integrated react-select into my code: import React, {Component} from 'react'; import Select, {createFilter} from 'react-select'; let _ = require('underscore') class Test extends Component { constructor(props) ...

Creating a stand-alone JavaScript file for generating Bootstrap-vue Toast notifications

After a few days of learning vue.js, I decided to implement a custom toast function based on the official bootstrap-vue documentation: . I successfully created toasts using component instance injection and custom components in Vue. However, my goal now is ...

Troubleshooting MaterialUI Datatable in Reactjs: How to Fix the Refresh Issue

Currently, I am facing an issue with rendering a DataTable component. The problem is that when I click on the "Users" button, it should display a table of Users, and when I click on the "Devices" button, it should show a table of Devices. But inexplicably, ...

Is it possible to detect a specific string being typed by the user in jQuery?

Similar to the way Facebook reacts when you mention a username by typing @username, how can jQuery be used to set up an event listener for [text:1]? I aim to trigger an event when the user types in [text: into a text field. ...

Collaborative Desktop & Web Application powered by Javascript REST/Ajax

Currently, my node.js back-end is seamlessly working with a web-based JavaScript client by sending AJAX requests. However, I am now contemplating creating a compact desktop version of the JavaScript client using solely JavaScript, specifically leveraging ...