From 26d5690c36795557a7597fdf881dac24264e0dc9 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 8 Dec 2017 08:23:52 +0100 Subject: [PATCH] MOBILE-2253 login: Restore session in init, handle SSO and other events --- src/app/app.component.ts | 66 +++++++- src/app/app.module.ts | 23 ++- src/classes/site.ts | 69 ++++---- src/core/login/providers/helper.ts | 264 ++++++++++++++++++++++++++++- src/providers/events.ts | 2 + src/providers/utils/utils.ts | 16 +- 6 files changed, 391 insertions(+), 49 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 9d2b521c1..a51f06f3f 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -12,24 +12,82 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component } from '@angular/core'; -import { Platform } from 'ionic-angular'; +import { Component, ViewChild, AfterViewInit } from '@angular/core'; +import { Platform, Nav } from 'ionic-angular'; import { StatusBar } from '@ionic-native/status-bar'; import { SplashScreen } from '@ionic-native/splash-screen'; +import { CoreEventsProvider } from '../providers/events'; +import { CoreLoginHelperProvider } from '../core/login/providers/helper'; @Component({ templateUrl: 'app.html' }) -export class MyApp { +export class MyApp implements AfterViewInit { + @ViewChild(Nav) navCtrl; rootPage:any = 'CoreLoginInitPage'; - constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen) { + constructor(private platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen, + private eventsProvider: CoreEventsProvider, private loginHelper: CoreLoginHelperProvider) { platform.ready().then(() => { // Okay, so the platform is ready and our plugins are available. // Here you can do any higher level native things you might need. statusBar.styleDefault(); splashScreen.hide(); }); + + } + + /** + * View has been initialized. + */ + ngAfterViewInit() { + this.loginHelper.setNavCtrl(this.navCtrl); + + // Go to sites page when user is logged out. + this.eventsProvider.on(CoreEventsProvider.LOGOUT, () => { + this.navCtrl.setRoot('CoreLoginSitesPage'); + }); + + // Listen for session expired events. + this.eventsProvider.on(CoreEventsProvider.SESSION_EXPIRED, (data) => { + this.loginHelper.sessionExpired(data); + }); + + // Listen for passwordchange and usernotfullysetup events to open InAppBrowser. + this.eventsProvider.on(CoreEventsProvider.PASSWORD_CHANGE_FORCED, (data) => { + this.loginHelper.openInAppForEdit(data.siteId, '/login/change_password.php', 'core.forcepasswordchangenotice', true); + }); + this.eventsProvider.on(CoreEventsProvider.USER_NOT_FULLY_SETUP, (data) => { + this.loginHelper.openInAppForEdit(data.siteId, '/user/edit.php', 'core.usernotfullysetup'); + }); + + // Listen for sitepolicynotagreed event to accept the site policy. + this.eventsProvider.on(CoreEventsProvider.SITE_POLICY_NOT_AGREED, (data) => { + this.loginHelper.sitePolicyNotAgreed(data.siteId); + }); + + // Check URLs loaded in any InAppBrowser. + this.eventsProvider.on(CoreEventsProvider.IAB_LOAD_START, (event) => { + this.loginHelper.inAppBrowserLoadStart(event.url); + }); + + // Check InAppBrowser closed. + this.eventsProvider.on(CoreEventsProvider.IAB_EXIT, () => { + this.loginHelper.waitingForBrowser = false; + this.loginHelper.lastInAppUrl = ''; + this.loginHelper.checkLogout(); + }); + + this.platform.resume.subscribe(() => { + // Wait a second before setting it to false since in iOS there could be some frozen WS calls. + setTimeout(() => { + this.loginHelper.waitingForBrowser = false; + this.loginHelper.checkLogout(); + }, 1000); + }); + + // @todo: Register observer to check if the app was launched via URL scheme. + // $mmURLDelegate.register('mmLoginSSO', appLaunchedByURL); } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2653b5470..5bf196048 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -53,6 +53,8 @@ import { CoreFilepoolProvider } from '../providers/filepool'; import { CoreUpdateManagerProvider } from '../providers/update-manager'; import { CorePluginFileDelegate } from '../providers/plugin-file-delegate'; +import { CoreLoginModule } from '../core/login/login.module'; + // For translate loader. AoT requires an exported function for factories. export function createTranslateLoader(http: HttpClient) { return new TranslateHttpLoader(http, './assets/lang/', '.json'); @@ -76,7 +78,8 @@ export function createTranslateLoader(http: HttpClient) { deps: [HttpClient] } }), - CoreEmulatorModule + CoreEmulatorModule, + CoreLoginModule ], bootstrap: [IonicApp], entryComponents: [ @@ -119,19 +122,27 @@ export function createTranslateLoader(http: HttpClient) { ] }) export class AppModule { - constructor(platform: Platform, initDelegate: CoreInitDelegate, updateManager: CoreUpdateManagerProvider) { - // Create a handler for platform ready and register it in the init delegate. - let handler = { + constructor(platform: Platform, initDelegate: CoreInitDelegate, updateManager: CoreUpdateManagerProvider, + sitesProvider: CoreSitesProvider) { + // Register a handler for platform ready. + initDelegate.registerProcess({ name: 'CorePlatformReady', priority: CoreInitDelegate.MAX_RECOMMENDED_PRIORITY + 400, blocking: true, load: platform.ready - }; - initDelegate.registerProcess(handler); + }); // Register the update manager as an init process. initDelegate.registerProcess(updateManager); + // Restore the user's session during the init process. + initDelegate.registerProcess({ + name: 'CoreRestoreSession', + priority: CoreInitDelegate.MAX_RECOMMENDED_PRIORITY + 200, + blocking: false, + load: sitesProvider.restoreSession.bind(sitesProvider) + }); + // Execute the init processes. initDelegate.executeInitProcesses(); } diff --git a/src/classes/site.ts b/src/classes/site.ts index 45911a296..d2ae143ec 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -30,6 +30,7 @@ import { CoreUtilsProvider } from '../providers/utils/utils'; import { CoreConstants } from '../core/constants'; import { CoreConfigConstants } from '../configconstants'; import { Md5 } from 'ts-md5/dist/md5'; +import { InAppBrowserObject } from '@ionic-native/in-app-browser'; export interface CoreSiteWSPreSets { getFromCache?: boolean; // Get the value from the cache if it's still valid. @@ -1017,9 +1018,9 @@ export class CoreSite { * @param {string} url The URL to open. * @param {any} [options] Override default options passed to InAppBrowser. * @param {string} [alertMessage] If defined, an alert will be shown before opening the inappbrowser. - * @return {Promise} Promise resolved when done, rejected otherwise. + * @return {Promise} Promise resolved when done. */ - openInAppWithAutoLogin(url: string, options?: any, alertMessage?: string) : Promise { + openInAppWithAutoLogin(url: string, options?: any, alertMessage?: string) : Promise { return this.openWithAutoLogin(true, url, options, alertMessage); } @@ -1029,9 +1030,9 @@ export class CoreSite { * @param {string} url The URL to open. * @param {object} [options] Override default options passed to inappbrowser. * @param {string} [alertMessage] If defined, an alert will be shown before opening the inappbrowser. - * @return {Promise} Promise resolved when done, rejected otherwise. + * @return {Promise} Promise resolved when done. */ - openInAppWithAutoLoginIfSameSite(url: string, options?: any, alertMessage?: string) : Promise { + openInAppWithAutoLoginIfSameSite(url: string, options?: any, alertMessage?: string) : Promise { return this.openWithAutoLoginIfSameSite(true, url, options, alertMessage); } @@ -1042,39 +1043,40 @@ export class CoreSite { * @param {string} url The URL to open. * @param {object} [options] Override default options passed to $cordovaInAppBrowser#open. * @param {string} [alertMessage] If defined, an alert will be shown before opening the browser/inappbrowser. - * @return {Promise} Promise resolved when done, rejected otherwise. + * @return {Promise} Promise resolved when done. Resolve param is returned only if inApp=true. */ - openWithAutoLogin(inApp: boolean, url: string, options?: any, alertMessage?: string) : Promise { + openWithAutoLogin(inApp: boolean, url: string, options?: any, alertMessage?: string) : Promise { // Convenience function to open the URL. let open = (url) => { - if (modal) { - modal.dismiss(); - } - - if (alertMessage) { - let alert = this.domUtils.showAlert('mm.core.notice', alertMessage, null, 3000); - alert.onDidDismiss(() => { - if (inApp) { - this.utils.openInApp(url, options); - } else { - this.utils.openInBrowser(url); - } - }); - } else { - if (inApp) { - this.utils.openInApp(url, options); - } else { - this.utils.openInBrowser(url); + return new Promise((resolve, reject) => { + if (modal) { + modal.dismiss(); } - } + + if (alertMessage) { + let alert = this.domUtils.showAlert('mm.core.notice', alertMessage, null, 3000); + alert.onDidDismiss(() => { + if (inApp) { + resolve(this.utils.openInApp(url, options)); + } else { + resolve(this.utils.openInBrowser(url)); + } + }); + } else { + if (inApp) { + resolve(this.utils.openInApp(url, options)); + } else { + resolve(this.utils.openInBrowser(url)); + } + } + }); }; if (!this.privateToken || !this.wsAvailable('tool_mobile_get_autologin_key') || (this.lastAutoLogin && this.timeUtils.timestamp() - this.lastAutoLogin < 6 * CoreConstants.secondsMinute)) { // No private token, WS not available or last auto-login was less than 6 minutes ago. // Open the final URL without auto-login. - open(url); - return Promise.resolve(); + return Promise.resolve(open(url)); } const userId = this.getUserId(), @@ -1087,16 +1089,15 @@ export class CoreSite { return this.write('tool_mobile_get_autologin_key', params).then((data) => { if (!data.autologinurl || !data.key) { // Not valid data, open the final URL without auto-login. - open(url); - return; + return open(url); } this.lastAutoLogin = this.timeUtils.timestamp(); - open(data.autologinurl + '?userid=' + userId + '&key=' + data.key + '&urltogo=' + url); + return open(data.autologinurl + '?userid=' + userId + '&key=' + data.key + '&urltogo=' + url); }).catch(() => { // Couldn't get autologin key, open the final URL without auto-login. - open(url); + return open(url); }); } @@ -1107,9 +1108,9 @@ export class CoreSite { * @param {string} url The URL to open. * @param {object} [options] Override default options passed to inappbrowser. * @param {string} [alertMessage] If defined, an alert will be shown before opening the browser/inappbrowser. - * @return {Promise} Promise resolved when done, rejected otherwise. + * @return {Promise} Promise resolved when done. Resolve param is returned only if inApp=true. */ - openWithAutoLoginIfSameSite(inApp: boolean, url: string, options?: any, alertMessage?: string) : Promise { + openWithAutoLoginIfSameSite(inApp: boolean, url: string, options?: any, alertMessage?: string) : Promise { if (this.containsUrl(url)) { return this.openWithAutoLogin(inApp, url, options, alertMessage); } else { @@ -1118,7 +1119,7 @@ export class CoreSite { } else { this.utils.openInBrowser(url); } - return Promise.resolve(); + return Promise.resolve(null); } } diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index 1b2decb2c..388d9e449 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -13,11 +13,12 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { NavController } from 'ionic-angular'; +import { NavController, Platform } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '../../../providers/app'; import { CoreConfigProvider } from '../../../providers/config'; import { CoreEventsProvider } from '../../../providers/events'; +import { CoreInitDelegate } from '../../../providers/init'; import { CoreLoggerProvider } from '../../../providers/logger'; import { CoreSitesProvider } from '../../../providers/sites'; import { CoreWSProvider } from '../../../providers/ws'; @@ -44,12 +45,17 @@ export interface CoreLoginSSOData { @Injectable() export class CoreLoginHelperProvider { protected logger; + protected isSSOConfirmShown = false; + protected isOpenEditAlertShown = false; + protected navCtrl: NavController; + lastInAppUrl: string; + waitingForBrowser = false; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider, private wsProvider: CoreWSProvider, private translate: TranslateService, private textUtils: CoreTextUtilsProvider, private eventsProvider: CoreEventsProvider, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider, - private urlUtils: CoreUrlUtilsProvider,private configProvider: CoreConfigProvider, - private emulatorHelper: CoreEmulatorHelperProvider) { + private urlUtils: CoreUrlUtilsProvider,private configProvider: CoreConfigProvider, private platform: Platform, + private emulatorHelper: CoreEmulatorHelperProvider, private initDelegate: CoreInitDelegate) { this.logger = logger.getInstance('CoreLoginHelper'); } @@ -84,6 +90,75 @@ export class CoreLoginHelperProvider { }); } + /** + * Function to handle URL received by Custom URL Scheme. If it's a SSO login, perform authentication. + * + * @param {string} url URL received. + * @return {boolean} True if it's a SSO URL, false otherwise. + */ + appLaunchedByURL(url: string) : boolean { + let ssoScheme = CoreConfigConstants.customurlscheme + '://token='; + if (url.indexOf(ssoScheme) == -1) { + return false; + } + + if (this.appProvider.isSSOAuthenticationOngoing()) { + // Authentication ongoing, probably duplicated request. + return true; + } + if (this.appProvider.isDesktop()) { + // In desktop, make sure InAppBrowser is closed. + this.utils.closeInAppBrowser(true); + } + + // App opened using custom URL scheme. Probably an SSO authentication. + this.appProvider.startSSOAuthentication(); + this.logger.debug('App launched by URL'); + + // Delete the sso scheme from the URL. + url = url.replace(ssoScheme, ''); + + // Some platforms like Windows add a slash at the end. Remove it. + // Some sites add a # at the end of the URL. If it's there, remove it. + url = url.replace(/\/?#?\/?$/, ''); + + // Decode from base64. + try { + url = atob(url); + } catch(err) { + // Error decoding the parameter. + this.logger.error('Error decoding parameter received for login SSO'); + return false; + } + + let modal = this.domUtils.showModalLoading('mm.login.authenticating', true), + siteData: CoreLoginSSOData; + + // Wait for app to be ready. + this.initDelegate.ready().then(() => { + return this.validateBrowserSSOLogin(url); + }).then((data) => { + siteData = data; + return this.handleSSOLoginAuthentication(siteData.siteUrl, siteData.token, siteData.privateToken); + }).then(() => { + if (siteData.pageName) { + // State defined, go to that state instead of site initial page. + this.navCtrl.push(siteData.pageName, siteData.pageParams); + } else { + this.goToSiteInitialPage(this.navCtrl, true); + } + }).catch((errorMessage) => { + if (typeof errorMessage == 'string' && errorMessage != '') { + this.domUtils.showErrorModal(errorMessage); + } + }).finally(() => { + modal.dismiss(); + this.appProvider.finishSSOAuthentication(); + }); + + return true; + } + /** * Check if a site allows requesting a password reset through the app. * @@ -98,6 +173,17 @@ export class CoreLoginHelperProvider { }); } + /** + * Function called when an SSO InAppBrowser is closed or the app is resumed. Check if user needs to be logged out. + */ + checkLogout() { + if (!this.appProvider.isSSOAuthenticationOngoing() && this.sitesProvider.isLoggedIn() && + this.sitesProvider.getCurrentSite().isLoggedOut() && this.navCtrl.getActive().name == 'CoreLoginReconnectPage') { + // User must reauthenticate but he closed the InAppBrowser without doing so, logout him. + this.sitesProvider.logout(); + } + } + /** * Show a confirm modal if needed and open a browser to perform SSO login. * @@ -374,6 +460,39 @@ export class CoreLoginHelperProvider { CoreConfigConstants.siteurl.length > 1; } + /** + * Function called when a page starts loading in any InAppBrowser window. + * + * @param {string} url Loaded url. + */ + inAppBrowserLoadStart(url: string) : void { + // URLs with a custom scheme can be prefixed with "http://" or "https://", we need to remove this. + url = url.replace(/^https?:\/\//, ''); + + if (this.appLaunchedByURL(url)) { + // Close the browser if it's a valid SSO URL. + this.utils.closeInAppBrowser(false); + } else if (this.platform.is('android')) { + // Check if the URL has a custom URL scheme. In Android they need to be opened manually. + const urlScheme = this.urlUtils.getUrlProtocol(url); + if (urlScheme && urlScheme !== 'file' && urlScheme !== 'cdvfile') { + // Open in browser should launch the right app if found and do nothing if not found. + this.utils.openInBrowser(url); + + // At this point the InAppBrowser is showing a "Webpage not available" error message. + // Try to navigate to last loaded URL so this error message isn't found. + if (this.lastInAppUrl) { + this.utils.openInApp(this.lastInAppUrl); + } else { + // No last URL loaded, close the InAppBrowser. + this.utils.closeInAppBrowser(false); + } + } else { + this.lastInAppUrl = url; + } + } + } + /** * Given a site public config, check if email signup is disabled. * @@ -580,6 +699,43 @@ export class CoreLoginHelperProvider { this.utils.openInApp(siteUrl + '/login/forgot_password.php'); } + /* + * Function to open in app browser to change password or complete user profile. + * + * @param {string} siteId The site ID. + * @param {string} path The relative path of the URL to open. + * @param {string} alertMessage The key of the message to display before opening the in app browser. + * @param {boolean} [invalidateCache] Whether to invalidate site's cache (e.g. when the user is forced to change password). + */ + openInAppForEdit(siteId: string, path: string, alertMessage: string, invalidateCache?: boolean) : void { + if (!siteId || siteId !== this.sitesProvider.getCurrentSiteId()) { + // Site that triggered the event is not current site, nothing to do. + return; + } + + let currentSite = this.sitesProvider.getCurrentSite(), + siteUrl = currentSite && currentSite.getURL(); + if (!currentSite || !siteUrl) { + return; + } + + if (!this.isOpenEditAlertShown && !this.waitingForBrowser) { + this.isOpenEditAlertShown = true; + + if (invalidateCache) { + currentSite.invalidateWsCache(); + } + + // Open change password. + alertMessage = this.translate.instant(alertMessage) + '
' + this.translate.instant('mm.core.redirectingtosite'); + currentSite.openInAppWithAutoLogin(siteUrl + path, undefined, alertMessage).then(() => { + this.waitingForBrowser = true; + }).finally(() => { + this.isOpenEditAlertShown = false; + }); + } + } + /** * Prepare the app to perform SSO login. * @@ -633,6 +789,88 @@ export class CoreLoginHelperProvider { return this.wsProvider.callAjax('core_auth_request_password_reset', params, {siteUrl: siteUrl}); } + /** + * Function that should be called when the session expires. Reserved for core use. + * + * @param {any} data Data received by the SESSION_EXPIRED event. + */ + sessionExpired(data: any) : void { + let siteId = data && data.siteId, + currentSite = this.sitesProvider.getCurrentSite(), + siteUrl = currentSite && currentSite.getURL(), + promise; + + if (!currentSite || !siteUrl) { + return; + } + + if (siteId && siteId !== currentSite.getId()) { + return; // Site that triggered the event is not current site. + } + + // Check authentication method. + this.sitesProvider.checkSite(siteUrl).then((result) => { + + if (result.warning) { + this.domUtils.showErrorModal(result.warning, true, 4000); + } + + if (this.isSSOLoginNeeded(result.code)) { + // SSO. User needs to authenticate in a browser. Prevent showing the message several times + // or show it again if the user is already authenticating using SSO. + if (!this.appProvider.isSSOAuthenticationOngoing() && !this.isSSOConfirmShown && !this.waitingForBrowser) { + this.isSSOConfirmShown = true; + + if (this.shouldShowSSOConfirm(result.code)) { + promise = this.domUtils.showConfirm(this.translate.instant('mm.login.' + + (currentSite.isLoggedOut() ? 'loggedoutssodescription' : 'reconnectssodescription'))); + } else { + promise = Promise.resolve(); + } + + promise.then(() => { + this.waitingForBrowser = true; + this.openBrowserForSSOLogin(result.siteUrl, result.code, result.service, + result.config && result.config.launchurl, data.pageName, data.params); + }).catch(() => { + // User cancelled, logout him. + this.sitesProvider.logout(); + }).finally(() => { + this.isSSOConfirmShown = false; + }); + } + } else { + let info = currentSite.getInfo(); + if (typeof info != 'undefined' && typeof info.username != 'undefined') { + this.navCtrl.setRoot('CoreLoginReconnectPage', { + infoSiteUrl: info.siteurl, + siteUrl: result.siteUrl, + siteId: siteId, + pageName: data.pageName, + pageParams: data.params, + siteConfig: result.config + }); + } + } + }).catch((error) => { + // Error checking site. + if (currentSite.isLoggedOut()) { + // Site is logged out, show error and logout the user. + this.domUtils.showErrorModalDefault(error, 'mm.core.networkerrormsg', true); + this.sitesProvider.logout(); + } + }); + } + + /** + * Set a NavController to use. + * + * @param {NavController} navCtrl Nav controller. + */ + setNavCtrl(navCtrl: NavController) : void { + this.navCtrl = navCtrl; + } + /** * Check if a confirm should be shown to open a SSO authentication. * @@ -644,6 +882,26 @@ export class CoreLoginHelperProvider { (!CoreConfigConstants.skipssoconfirmation || String(CoreConfigConstants.skipssoconfirmation) === 'false'); } + /** + * Function called when site policy is not agreed. Reserved for core use. + * + * @param {string} [siteId] Site ID. If not defined, current site. + */ + sitePolicyNotAgreed(siteId?: string) : void { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + if (!siteId || siteId != this.sitesProvider.getCurrentSiteId()) { + // Only current site allowed. + return; + } + + if (!this.sitesProvider.getCurrentSite().wsAvailable('core_user_agree_site_policy')) { + // WS not available, stop. + return; + } + + this.navCtrl.setRoot('CoreLoginSitePolicyPage', {siteId: siteId}); + } + /** * Convenient helper to handle get User Token error. It redirects to change password page if forcepassword is set. * diff --git a/src/providers/events.ts b/src/providers/events.ts index 779f103dd..a153b1112 100644 --- a/src/providers/events.ts +++ b/src/providers/events.ts @@ -43,6 +43,8 @@ export class CoreEventsProvider { public static REMOTE_ADDONS_LOADED = 'remote_addons_loaded'; public static LOGIN_SITE_CHECKED = 'login_site_checked'; public static LOGIN_SITE_UNCHECKED = 'login_site_unchecked'; + public static IAB_LOAD_START = 'inappbrowser_load_start'; + public static IAB_EXIT = 'inappbrowser_exit'; logger; observables = {}; diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index ea2423c27..6cfb1ba4f 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -19,6 +19,7 @@ import { InAppBrowser, InAppBrowserObject } from '@ionic-native/in-app-browser'; import { Clipboard } from '@ionic-native/clipboard'; import { CoreAppProvider } from '../app'; import { CoreDomUtilsProvider } from './dom'; +import { CoreEventsProvider } from '../events'; import { CoreLoggerProvider } from '../logger'; import { TranslateService } from '@ngx-translate/core'; import { CoreLangProvider } from '../lang'; @@ -39,7 +40,7 @@ export class CoreUtilsProvider { constructor(private iab: InAppBrowser, private appProvider: CoreAppProvider, private clipboard: Clipboard, private domUtils: CoreDomUtilsProvider, logger: CoreLoggerProvider, private translate: TranslateService, - private platform: Platform, private langProvider: CoreLangProvider) { + private platform: Platform, private langProvider: CoreLangProvider, private eventsProvider: CoreEventsProvider) { this.logger = logger.getInstance('CoreUtilsProvider'); } @@ -249,7 +250,7 @@ export class CoreUtilsProvider { * * @param {boolean} [closeAll] Desktop only. True to close all secondary windows, false to close only the "current" one. */ - closeInAppBrowser(closeAll: boolean) : void { + closeInAppBrowser(closeAll?: boolean) : void { if (this.iabInstance) { this.iabInstance.close(); if (closeAll && this.appProvider.isDesktop()) { @@ -790,6 +791,17 @@ export class CoreUtilsProvider { } this.iabInstance = this.iab.create(url, '_blank', options); + + // Trigger global events when a url is loaded or the window is closed. This is to make it work like in Ionic 1. + let loadStartSubscription = this.iabInstance.on('loadstart').subscribe((event) => { + this.eventsProvider.trigger(CoreEventsProvider.IAB_LOAD_START, event); + }); + let exitSubscription = this.iabInstance.on('exit').subscribe((event) => { + loadStartSubscription.unsubscribe(); + exitSubscription.unsubscribe(); + this.eventsProvider.trigger(CoreEventsProvider.IAB_EXIT, event); + }); + return this.iabInstance; }