Hoy voy a explicar como facilitar que los usuarios de una app PhoneGap, desarrollada utilizando Ionic framework, puedan registrarse en la app con el login de Google+.
Voy a realizar el tutorial para probarlo en plataforma Android, pero explicaré los pasos que habría que seguir también en iOS, aunque no lo probaré en un iOS real.
Si quieres trastear con el código, al final del artículo encontrarás el enlace al repositorio de github donde está alojado.
Registro de la app en el ecosistema Google
Lo primero que haré, es registrar la app en el ecosistema de Google+, ya que es requisito indispensable para utilizar la API de login de Google+.
Sigue el tutorial Registrar app en Google+ para autentificar usuarios paso a paso, y vuelve a aquí cuando lo hayas completado.
Desarrollo de la app
Doy por sentado que se tiene instalado ionic y un mínimo de conocimiento de como funciona. En caso contrario, te recomiendo leer por encima esto
Creación de la app
Me voy directo al terminal, y creo el proyecto googleLoginDemo a partir de un proyecto vacío:
$: ionic start googleLoginDemo blank
Una vez creado, entro en el proyecto:
$: cd googleLoginDemo
Añadiendo plataformas
Para añadir la plataforma Android, ejecuto en terminal:
googleLoginDemo$:ionic platform android
googleLoginDemo$:ionic build android
De forma equivalente, para iOS, tendría que ejecutar:
googleLoginDemo$:ionic platform ios
googleLoginDemo$:ionic build ios
Abrir el proyecto en Eclipse
Importo el proyecto a Eclipse con:
File > New > Project… > Android Project from Existing Code
Selecciono el directorio googleLoginDemo con mi proyecto.
DETALLE: Para que el proyecto se actualice con los cambios y pueda ejecutarlo correctamente desde Eclipse, antes de ejecutarlo tengo que ejecutar por consola: cordova prepare android, y posteriormente hacer un refresh en Eclipse.
Añadir el Package name
Anteriormente he registrado una app en Google, para autentificarme a través de Google+, y he definido un indentificador de aplicación tipo: com.enriqueoriol.googleLoginDemo.android
Ahora tengo que definir este mismo identificador en la app, y esto lo hago en la sección id del archivo en la raíz de mi directorio config.xml.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<widget id="com.enriqueoriol.googleLoginDemo.android" ...
DETALLE: Al reemplazarlo en el config.xml, ionic actualizará automáticamente el archivo AndroidManifest.xml cada vez que lo ejecutemos. Si lo reemplazas directamente en el manifest, tu nombre se sobreescribirá y no podrás ejecutar la app.
Estructura de la app
Lo que voy a crear es una app con 2 vistas, gestionadas por una barra de navegación.
- La primera vista, se cargará por defecto, y tendrá un botón de login de Google+
- Una vez logueados nos dará paso a la segunda vista, con información del usuario, y que tendrá un botón para hacer logout
Aunque se trata de un proyecto sencillo, me gusta tener bien ordenado el código, por lo que crearé una carpeta para cada una de estas vistas, donde irá su template y su javascript.
De momento, resumo que la estructura será:
www/
index.html
app/
app.js
login/
login.html
login.js
userInfo/
userInfo.html
userInfo.js
css/
style.css
img/
lib/
…
app.js
El proyecto blank en el que está basado mi proyecto, ya ha generado un archivo js/app.js con la inicialización más básica. Como ves de la estructura anterior, yo lo muevo a la carpeta app que he creado, y le añado configuración de estados para definir un estado asociado a cada vista.
Queda así:
// Google Login Demo App
angular.module('googleLoginDemo', ['ionic', 'login', 'userInfo'])
.run(function($ionicPlatform) {
$ionicPlatform.ready(function() {
// Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
// for form inputs)
if(window.cordova && window.cordova.plugins.Keyboard) {
cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
}
if(window.StatusBar) {
StatusBar.styleDefault();
}
});
})
.config(function($stateProvider, $urlRouterProvider){
$urlRouterProvider.otherwise('/')
$stateProvider
.state('login', {
url: '/',
controller: 'LoginCtrl',
templateUrl: 'app/login/login.html'
})
.state('userInfo', {
url: '/userInfo',
controller: 'UserInfoCtrl',
templateUrl: 'app/userInfo/userInfo.html'
});
})
A destacar:
- Incluyo los módulos login y userInfo. Los definiré en breve, y son los que tendrán la funcionalidad de cada una de las vistas respectivamente.
- Creo los estados login y userInfo. Los estados permiten identificar una vista y vincularla a una url, definir el controlador que se va a usar, etc.
Más adelante veré como dirigirnos a una vista con $state.go(‘nombreEstado’);
Index.html
El proyecto blank tamibén ha generado un archivo index.html con lo básico (librerías y css requeridos). Yo voy a cambiarle el body, y reemplazarlo por un simple ion-nav-view, que es un espacio que se reemplaza por las ion-view que contendrán mis vistas. Además, también añado una barra de navegación ion-nav-bar, y un botón atrás ion-nav-back-button, para poder volver de la segunda vista a la primera.
Queda así:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
<title>google Login Demo Project</title>
<link href="lib/ionic/css/ionic.css" rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
<script src="lib/ionic/js/ionic.bundle.js"></script>
<!-- cordova script (this will be a 404 during development) -->
<script src="cordova.js"></script>
<!-- your app's js -->
<script src="app/app.js"></script>
<script src="app/login/login.js"></script>
<script src="app/userInfo/userInfo.js"></script>
</head>
<body ng-app="googleLoginDemo">
<ion-nav-bar class="bar-positive">
<ion-nav-back-button class="button-clear">
</ion-nav-back-button>
</ion-nav-bar>
<ion-nav-view></ion-nav-view>
</body>
</html>
Importante: Es imprescindible que incluya, como hago arriba, los scripts JS login.js y userInfo.js donde quiero definir el comportamiento de cada vista.
Vista de login
Template
Creo un template muy sencillo, en app/login/login.js:
<ion-view title="Login">
<ion-content>
<h3 class="padding">Let's try Google + Login!</h3>
<br/>
<button class="button button-assertive button-block padding" ng-click="login()">
Google+ login!
</button>
<br/>
</ion-content>
</ion-view>
Se trata de un simple botón, que llama a la función login() al hacer click.
Módulo y controlador
Implemento la lógica (de momento vacía) en app/login/login.js:
angular.module('login', [])
.controller('LoginCtrl', function($scope, $state) {
var initView = function(){
//Do any expected initialization
}
$scope.$on('$ionicView.beforeEnter', function(){
initView();
});
$scope.login = function(){
//TODO: perform login operation
$state.go('userInfo');
}
})
Básicamente creo una función login en el scope, que de momento lo que hace es enviarme a la otra vista.
¿Qué aspecto tiene esto? Sencillo, pero suficiente para lo que queremos.
Ahí lo ves:
Vista de información
Template
Creo un template muy sencillo, en app/userInfo/userInfo.js:
<ion-view title="User Info">
<ion-content>
<br/>
<div class="padding">
<span style="font-width:strong; font-size:large"> You're logged as:</span> {{userName}}
</div>
<button class="button button-assertive button-block padding" ng-click="logout()">Log out!</button>
<br/>
</ion-content>
</ion-view>
En este caso, en la primera linea muestro el nombre de usuario, y en la siguiente muestro un botón para hacer logout.
Módulo y controlador
Implemento la lógica (de momento vacía) en app/userInfo/userInfo.js:
angular.module('userInfo', [])
.controller('UserInfoCtrl', function($scope, $state) {
$scope.userName = "Demo user";
var initView = function(){
//Do any expected initialization
}
$scope.$on('$ionicView.beforeEnter', function(){
initView();
});
$scope.logout = function(){
//perform logout operation
$state.go('login');
}
})
De momento, para que todo funcione, creo en el $scope un nombre de usuario por defecto, y una función logout que me devuelve a la vista anterior.
¿Y como queda esta vista?
Pues parecida a la anterior:
Probando la estructura
Ya hemos visto los pantallazos, pero aquí te dejo una demo interactiva:
See the Pen Ionin Navigation between 2 views demo by Enrique (@-kaik-) on CodePen.
Integrando autentificación con Google+
Ya tengo la estructura de la app, ahora voy a entrar en materia para integrar la autentificación de Google+.
Plugin de Google+
Lo primero es instalarse el plugin oficial de Google+ para Córdova/PhoneGap
Me instalo el plugin del siguiente modo:
googleLoginDemo$:cordova plugin add nl.x-services.plugins.googleplus
googleLoginDemo$:cordova prepare
Una vez hecho esto, el código de GooglePlus.js se añade automáticamente, por lo que no tenemos que tocar nada de nuestro html.
IMPORTANTE: Ninguno de estos métodos puede llamarse antes de que se invoque el evento deviceready (en nuestro caso, con ionic, podemos usar su equivalente $ionicPlatform.ready).
Este plugin te proporciona diversas funcionalidades, vamos a ver algunas:
isAvailable
Si quiero poner un botón de login con Google+, mejor que compruebe esto antes.
- En iOS comprueba si Google+ está instalado, si no lo está y llamamos a la función de login, se redirigirá a Safari, que según comenta el creador del plugin, puede ser motivo de rechazo de una app en la app store.
- En Android, comprueba si Google Play Services está instalado (probablemente lo esté).
Login
Intenta hacer el login a partir de la cuenta de Google+ que tengamos registrada en el dispositivo. No abre una vista para seleccionar la cuenta que queremos usar, y nos pide permisos para autentificarnos con Google+.
En caso de éxito, el callback nos devuelve un objeto obj en JSON con el siguiente contenido:
obj.email
obj.userId
obj.displayName
obj.gender
obj.imageUrl
obj.givenName
obj.middleName
obj.familyName
obj.birthday
obj.ageRangeMin
obj.ageRangeMax
obj.idToken
obj.oauthToken
Try silent login
La idea es utilizar este método cuando el usuario vuelve a la app, y no estoy seguro de si necesita autentificación. Con ella, intento logear al usuario. Si tengo éxito, obtengo de nuevo un objeto como el anterior obj. Si no, tengo un callback de error para gestionarlo, pero no me aparece en ningún momento el diálogo de selección de cuenta de usuario.
Logout
Elimina el token OAuth2.
Disconnect
Elimina el token OAuth2 y se olvida de qué cuenta se ha utilizado para hacer login.
Servicio para gestionar la autentificación
Como a mi me gusta el código ordenado, y prefiero utilizar dependency injection en la medida de lo posible, creo un nuevo fichero (lo haré en la nueva carpeta services: services/SocialAuthService.js), que me devolverá un singleton SocialAuth, con los siguientes métodos:
- SocialAuth.getAuthUser: Me devuelve el objeto con el usuario, o bien null
- SocialAuth.isGooglePlusAvailable: Promise que devuelve parámetro true/false
- SocialAuth.googlePlusLogin: Promise con callbacks success/error
- SocialAuth.googlePlusSilentLogin: Promise con callbacks success/error
- SocialAuth.googlePlusLogout: Promise que devuelve un mensaje
- SocialAuth.googlePlusDisconnect: Promise que devuelve un mensaje
Dejo el código a continuación
services/SocialAuthService.js:
angular.module('SocialAuthService', [])
/**
* A service that handles methods related with Social authentication
*/
.factory('SocialAuth', function($http, $q) {
var SocialAuth = {};
var authUser = null;
var isGooglePlusAPIAvailableOrReject = function(deferred){
if(window.plugins == null || window.plugins.googleplus == null){
setTimeout(function(){
console.log("API not available");
deferred.reject("API not available");
}, 100);
return false;
}
return true;
}
/**
* @brief Static method that returns the authenticated user object
*
*/
SocialAuth.getAuthUser = function() {
return authUser;
};
/**
* @brief Static method that check whether google+ is available or not,
* returning a promise, with a unique parameter (true/false).
*
*/
SocialAuth.isGooglePlusAvailable = function() {
var deferred = $q.defer();
if(isGooglePlusAPIAvailableOrReject(deferred)){
window.plugins.googleplus.isAvailable(
function (available) {
deferred.resolve(available);
}
);
}
//return the promise object
return deferred.promise;
};
/**
* @brief Static method that attempts to log-in using Google+ auth API.
* This method returns a promise with success/error callbacks.
*
*/
SocialAuth.googlePlusLogin = function() {
var deferred = $q.defer();
if(isGooglePlusAPIAvailableOrReject(deferred)){
window.plugins.googleplus.login(
{
'iOSApiKey': 'my_iOS_API_KEY_if_I_have_one'
// there is no API key for Android; you app is wired to the Google+ API by listing
// your package name in the google dev console and signing your apk
},
function (obj) {
authUser = angular.fromJson(obj);
deferred.resolve(obj);
},
function (err) {
deferred.reject(err);
}
);
}
//get the promise object
var promise = deferred.promise;
//add success callback to the promise, and associate it with the RESOLVE call
promise.success = function(fn) {
return promise.then(function(response) {
fn(response);
})
}
//add success callback to the promise, and associate it with the REJECT call
promise.error = function(fn) {
return promise.then(null, function(response) {
fn(response);
})
}
//return the promise object
return promise;
};
/**
* @brief Static method that attempts to log-in silently using Google+ auth API.
* This method returns a promise with success/error callbacks.
* If it succeeds, you get the same object returned by googlePlusLogin. If it fails, it
* will not show the auth dialog to the user
*
* @returns obj The success callback gets a JSON object like the one returned by
* SocialAuth.googlePlusLogin
*
* @see SocialAuth.googlePlusLogin
*/
SocialAuth.googlePlusSilentLogin = function() {
var deferred = $q.defer();
if(isGooglePlusAPIAvailableOrReject(deferred)){
window.plugins.googleplus.trySilentLogin(
{
'iOSApiKey': 'my_iOS_API_KEY_if_I_have_one'
},
function (obj) {
console.log(obj);
authUser = angular.fromJson(obj);
deferred.resolve(obj);
},
function (err) {
deferred.reject(err);
}
);
}
//get the promise object
var promise = deferred.promise;
//add success callback to the promise, and associate it with the RESOLVE call
promise.success = function(fn) {
return promise.then(function(response) {
fn(response);
})
}
//add success callback to the promise, and associate it with the REJECT call
promise.error = function(fn) {
return promise.then(null, function(response) {
fn(response);
})
}
//return the promise object
return promise;
};
/**
* @brief Static method that attempts to clear the OAuth2 token
* returning a promise, with a unique message parameter
*
*/
SocialAuth.googlePlusLogout = function() {
var deferred = $q.defer();
if(isGooglePlusAPIAvailableOrReject(deferred)){
window.plugins.googleplus.logout(
function (msg) {
authUser = null;
deferred.resolve(msg);
}
);
}
//return the promise object
return deferred.promise;
};
/**
* @brief Static method that attempts to clear the OAuth2 token
* and forget which account was used to log in (this forces the user to re-auth the app again)
* returning a promise, with a unique message parameter
*
*/
SocialAuth.googlePlusDisconnect = function() {
var deferred = $q.defer();
if(isGooglePlusAPIAvailableOrReject(deferred)){
window.plugins.googleplus.disconnect(
function (msg) {
authUser = null;
deferred.resolve(msg);
}
);
}
//return the promise object
return deferred.promise;
};
return SocialAuth;
});
Usando el login de Google
Login.js
Voy a modificar el script login.js para loguearme con Google+.
En primer lugar, incluyo el servicio para poderlo usar
angular.module('login', ['SocialAuthService'])
.controller('LoginCtrl', function($ionicPlatform, $scope, $state, SocialAuth) {
...
}
A continuación, creo una función que comprobará si la API de Google+ está disponible:
- En caso negativo, oculta el botón de login
- En caso afirmativo, intenta el login silencioso
- Si lo consigue, pasa a la vista de usuario
- Si no lo consigue, muestra el botón de login
var silentLoginAttempt = function(){
SocialAuth.isGooglePlusAvailable()
.then(function(available){
console.log("google plus availability is: " + available);
if(!available){
$scope.showGoogleLogin = false;
}
else{
var promise = SocialAuth.googlePlusSilentLogin();
promise.success(function(msg){
console.log("silent login success");
$state.go('userInfo');
});
promise.error(function(err){
console.log("silent login failed: " + err);
$scope.showGoogleLogin = true;
});
}
});
}
Para que esto tenga sentido, haremos cambios en la vista, que veremos más adelante.
Siguiendo con el javascript, actualizo la función initView de login.js. Ahora, si estoy logueado automáticamente pasa a la siguiente vista. Si no, llama a la función que acabo de crear para intentar el login silencioso:
var initView = function(){
$scope.showGoogleLogin = false;
$ionicPlatform.ready(function(){
if(SocialAuth.getAuthUser() == null){
silentLoginAttempt();
}
else{
$state.go('userInfo');
}
})
}
$scope.$on('$ionicView.beforeEnter', function(){
initView();
});
Finalmente, en la función de login(), utilizo de nuevo mi servicio SocialAuth
$scope.login = function(){
var promise = SocialAuth.googlePlusLogin();
promise.success(function(msg){
$state.go('userInfo');
});
promise.error(function(err){
alert("Invalid login!! Error: " + err);
});
}
Login.html
Como he comentado, tengo que actualizar la vista de login para utilizar la nueva variable que oculta o no el botón. Yo la dejo así.
<ion-view title="Login">
<ion-content>
<h3 class="padding">This view tries to log-in with Google+</h3>
<br/>
<div ng-show="showGoogleLogin" class="padding">You're logged out. Please, log-in again</div>
<button ng-show="showGoogleLogin" class="button button-assertive button-block padding" ng-click="login()">
Google+ login!
</button>
<h4 class="padding" ng-hide="showGoogleLogin">Sorry, you cannot login with Google+</h4>
<br/>
</ion-content>
</ion-view>
UserInfo.js
La gracia de estar logueado es que tengo acceso a datos de la cuenta de Google del usuario. Por un lado su información básica, que utilizaré ahora para mostrar el nombre de usuario, pero por otro a su token de autentificación.
DETALLE: El token OAuth2 es interesante, por que me permitirá realizar peticiones a la API de Google+ para recuperar más datos del usuario, como sus contactos (en función de los permisos que tenga nuestra app).
¿Como cambio el código en userInfo.js?
Tengo que importar el módulo y servicio, por supuesto.
Además recupero el nombre de usuario a través del servicio, y actualizo la función de logout() para que use SocialAuth.
angular.module('userInfo', ['SocialAuthService'])
.controller('UserInfoCtrl', function($scope, $state, SocialAuth) {
$scope.userName = "Demo user";
var initView = function(){
$scope.userName = SocialAuth.getAuthUser().displayName;
console.log("user oAuth token: " + SocialAuth.getAuthUser().oauthToken);
}
$scope.$on('$ionicView.beforeEnter', function(){
initView();
});
$scope.logout = function(){
SocialAuth.googlePlusLogout().then(function(value){
console.log("logout value: " + value);
$state.go('login');
})
}
})
Fíjate que no hace falta cambiar mi template userInfo.html, ya he estoy mostrando el nombre de usuario a partir de la variable de entorno $scope.userName
En el siguiente vídeo puedes ver como funciona en un Android real.
Repositorio github
Lo prometido es deuda. Si quieres trastear con el código, en mi repositorio github para demos de ionic, encontrarás el código utilizado, en la carpeta googleLoginDemo.
Si te ha gustado este artículo, ¡no olvides compartirlo!
Saludos