diff --git a/src/app/app.component.test.ts b/src/app/app.component.test.ts index 0d8dc9805..1a8d32c16 100644 --- a/src/app/app.component.test.ts +++ b/src/app/app.component.test.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Observable } from 'rxjs'; +import { Observable, Subject } from 'rxjs'; import { AppComponent } from '@/app/app.component'; import { CoreApp } from '@services/app'; @@ -31,7 +31,7 @@ describe('AppComponent', () => { beforeEach(() => { mockSingleton(CoreApp, { setStatusBarColor: jest.fn() }); mockSingleton(Network, { onChange: () => new Observable() }); - mockSingleton(Platform, { ready: () => Promise.resolve() }); + mockSingleton(Platform, { ready: () => Promise.resolve(), resume: new Subject() }); mockSingleton(NgZone, { run: jest.fn() }); navigator = mockSingleton(CoreNavigator, ['navigate']); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 71bd22964..96d103515 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -29,6 +29,10 @@ import { CoreApp } from '@services/app'; import { CoreSites } from '@services/sites'; import { CoreNavigator } from '@services/navigator'; import { CoreSubscriptions } from '@singletons/subscriptions'; +import { CoreWindow } from '@singletons/window'; +import { CoreCustomURLSchemes } from '@services/urlschemes'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreUrlUtils } from '@services/utils/url'; @Component({ selector: 'app-root', @@ -39,6 +43,9 @@ export class AppComponent implements OnInit, AfterViewInit { @ViewChild(IonRouterOutlet) outlet?: IonRouterOutlet; + protected lastUrls: Record = {}; + protected lastInAppUrl?: string; + /** * Component being initialized. * @@ -51,6 +58,9 @@ export class AppComponent implements OnInit, AfterViewInit { * - Note: HideKeyboardFormAccessoryBar has been moved to config.xml. */ ngOnInit(): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const win = window; + CoreEvents.on(CoreEvents.LOGOUT, () => { // Go to sites page when user is logged out. CoreNavigator.navigate('/login/sites', { reset: true }); @@ -80,6 +90,83 @@ export class AppComponent implements OnInit, AfterViewInit { CoreLoginHelper.sitePolicyNotAgreed(data.siteId); }); + // Check URLs loaded in any InAppBrowser. + CoreEvents.on(CoreEvents.IAB_LOAD_START, (event) => { + // URLs with a custom scheme can be prefixed with "http://" or "https://", we need to remove this. + const url = event.url.replace(/^https?:\/\//, ''); + + if (CoreCustomURLSchemes.isCustomURL(url)) { + // Close the browser if it's a valid SSO URL. + CoreCustomURLSchemes.handleCustomURL(url).catch((error) => { + CoreCustomURLSchemes.treatHandleCustomURLError(error); + }); + CoreUtils.closeInAppBrowser(); + + } else if (CoreApp.instance.isAndroid()) { + // Check if the URL has a custom URL scheme. In Android they need to be opened manually. + const urlScheme = CoreUrlUtils.getUrlProtocol(url); + if (urlScheme && urlScheme !== 'file' && urlScheme !== 'cdvfile') { + // Open in browser should launch the right app if found and do nothing if not found. + CoreUtils.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) { + CoreUtils.openInApp(this.lastInAppUrl); + } else { + // No last URL loaded, close the InAppBrowser. + CoreUtils.closeInAppBrowser(); + } + } else { + this.lastInAppUrl = url; + } + } + }); + + // Check InAppBrowser closed. + CoreEvents.on(CoreEvents.IAB_EXIT, () => { + CoreLoginHelper.setWaitingForBrowser(false); + this.lastInAppUrl = ''; + CoreLoginHelper.checkLogout(); + }); + + Platform.resume.subscribe(() => { + // Wait a second before setting it to false since in iOS there could be some frozen WS calls. + setTimeout(() => { + CoreLoginHelper.setWaitingForBrowser(false); + CoreLoginHelper.checkLogout(); + }, 1000); + }); + + // Handle app launched with a certain URL (custom URL scheme). + win.handleOpenURL = (url: string): void => { + // Execute the callback in the Angular zone, so change detection doesn't stop working. + NgZone.run(() => { + // First check that the URL hasn't been treated a few seconds ago. Sometimes this function is called more than once. + if (this.lastUrls[url] && Date.now() - this.lastUrls[url] < 3000) { + // Function called more than once, stop. + return; + } + + if (!CoreCustomURLSchemes.isCustomURL(url)) { + // Not a custom URL, ignore. + return; + } + + this.lastUrls[url] = Date.now(); + + CoreEvents.trigger(CoreEvents.APP_LAUNCHED_URL, url); + CoreCustomURLSchemes.handleCustomURL(url).catch((error) => { + CoreCustomURLSchemes.treatHandleCustomURLError(error); + }); + }); + }; + + // "Expose" CoreWindow.open. + win.openWindowSafely = (url: string, name?: string): void => { + CoreWindow.open(url, name); + }; + CoreEvents.on(CoreEvents.LOGIN, async (data: CoreEventSiteData) => { if (data.siteId) { const site = await CoreSites.getSite(data.siteId); diff --git a/src/core/core.module.ts b/src/core/core.module.ts index ec8c88235..5a5663887 100644 --- a/src/core/core.module.ts +++ b/src/core/core.module.ts @@ -47,12 +47,13 @@ import { CoreFileHelperProvider } from '@services/file-helper'; import { CoreGeolocationProvider } from '@services/geolocation'; import { CoreNavigatorService } from '@services/navigator'; import { CoreScreenService } from '@services/screen'; +import { CoreCustomURLSchemesProvider } from '@services/urlschemes'; export const CORE_SERVICES: Type[] = [ CoreAppProvider, CoreConfigProvider, CoreCronDelegateService, - // @todo CoreCustomURLSchemesProvider, + CoreCustomURLSchemesProvider, CoreDbProvider, CoreFileHelperProvider, CoreFileSessionProvider, diff --git a/src/core/directives/link.ts b/src/core/directives/link.ts index 0271143bc..2c125c170 100644 --- a/src/core/directives/link.ts +++ b/src/core/directives/link.ts @@ -23,6 +23,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreTextUtils } from '@services/utils/text'; import { CoreConstants } from '@/core/constants'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; +import { CoreCustomURLSchemes } from '@services/urlschemes'; /** * Directive to open a link in external browser or in the app. @@ -111,7 +112,15 @@ export class CoreLinkDirective implements OnInit { return; } - // @todo: Custom URL schemes. + if (CoreCustomURLSchemes.isCustomURL(href)) { + try { + await CoreCustomURLSchemes.handleCustomURL(href); + } catch (error) { + CoreCustomURLSchemes.treatHandleCustomURLError(error); + } + + return; + } return this.openExternalLink(href, openIn); } diff --git a/src/core/services/urlschemes.ts b/src/core/services/urlschemes.ts new file mode 100644 index 000000000..67b3924d9 --- /dev/null +++ b/src/core/services/urlschemes.ts @@ -0,0 +1,564 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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. + +import { Injectable } from '@angular/core'; + +import { CoreError } from '@classes/errors/error'; +import { CoreWSError } from '@classes/errors/wserror'; +import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; +import { CoreLoginHelper, CoreLoginSSOData } from '@features/login/services/login-helper'; +import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; +import { ApplicationInit, makeSingleton, Translate } from '@singletons'; +import { CoreLogger } from '@singletons/logger'; +import { CoreConstants } from '../constants'; +import { CoreApp } from './app'; +import { CoreNavigator } from './navigator'; +import { CoreSiteCheckResponse, CoreSites } from './sites'; +import { CoreDomUtils } from './utils/dom'; +import { CoreTextErrorObject, CoreTextUtils } from './utils/text'; +import { CoreUrlUtils } from './utils/url'; +import { CoreUtils } from './utils/utils'; + +/* + * Provider to handle custom URL schemes. + */ +@Injectable({ providedIn: 'root' }) +export class CoreCustomURLSchemesProvider { + + protected logger: CoreLogger; + protected lastUrls: Record = {}; + + constructor() { + this.logger = CoreLogger.getInstance('CoreCustomURLSchemesProvider'); + } + + /** + * Given some data of a custom URL with a token, create a site if it needs to be created. + * + * @param data URL data. + * @return Promise resolved with the site ID if created or already exists. + */ + protected async createSiteIfNeeded(data: CoreCustomURLSchemesParams): Promise { + if (!data.token) { + return; + } + + const currentSite = CoreSites.getCurrentSite(); + + if (!currentSite || currentSite.getToken() != data.token) { + // Token belongs to a different site, create it. It doesn't matter if it already exists. + + if (!data.siteUrl.match(/^https?:\/\//)) { + // URL doesn't have a protocol and it's required to be able to create the site. Check which one to use. + const result = await CoreSites.checkSite(data.siteUrl); + + data.siteUrl = result.siteUrl; + + await CoreSites.checkApplication(result); + } + + return CoreSites.newSite( + data.siteUrl, + data.token, + data.privateToken, + !!data.isSSOToken, + CoreLoginHelper.getOAuthIdFromParams(data.ssoUrlParams), + ); + } else { + // Token belongs to current site, no need to create it. + return CoreSites.getCurrentSiteId(); + } + } + + /** + * Handle an URL received by custom URL scheme. + * + * @param url URL to treat. + * @return Promise resolved when done. If rejected, the parameter is of type CoreCustomURLSchemesHandleError. + */ + async handleCustomURL(url: string): Promise { + if (!this.isCustomURL(url)) { + throw new CoreCustomURLSchemesHandleError(null); + } + + /* First check that this URL hasn't been treated a few seconds ago. The function that handles custom URL schemes already + does this, but this function is called from other places so we need to handle it in here too. */ + if (this.lastUrls[url] && Date.now() - this.lastUrls[url] < 3000) { + // Function called more than once, stop. + return; + } + + this.lastUrls[url] = Date.now(); + url = CoreTextUtils.decodeURIComponent(url); + + // Wait for app to be ready. + await ApplicationInit.donePromise; + + // 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(/\/?#?\/?$/, ''); + + const modal = await CoreDomUtils.showModalLoading(); + let data: CoreCustomURLSchemesParams; + + // Get the data from the URL. + try { + if (this.isCustomURLToken(url)) { + data = await this.getCustomURLTokenData(url); + } else if (this.isCustomURLLink(url)) { + // In iOS, the protocol after the scheme doesn't have ":". Add it. + url = url.replace(/\/\/link=(https?)\/\//, '//link=$1://'); + + data = await this.getCustomURLLinkData(url); + } else { + // In iOS, the protocol after the scheme doesn't have ":". Add it. + url = url.replace(/\/\/(https?)\/\//, '//$1://'); + + data = await this.getCustomURLData(url); + } + } catch (error) { + modal.dismiss(); + + throw error; + } + + try { + const isValid = await CoreLoginHelper.isSiteUrlAllowed(data.siteUrl); + + if (!isValid) { + throw Translate.instant('core.errorurlschemeinvalidsite'); + } + + if (data.redirect && data.redirect.match(/^https?:\/\//) && data.redirect.indexOf(data.siteUrl) == -1) { + // Redirect URL must belong to the same site. Reject. + throw Translate.instant('core.contentlinks.errorredirectothersite'); + } + + // First of all, create the site if needed. + const siteId = await this.createSiteIfNeeded(data); + + if (data.isSSOToken || (data.isAuthenticationURL && siteId && CoreSites.getCurrentSiteId() == siteId)) { + // Site created and authenticated, open the page to go. + if (data.pageName) { + // Page defined, go to that page instead of site initial page. + CoreNavigator.navigateToSitePath(data.pageName, { + params: data.pageParams, + }); + } else { + CoreNavigator.navigateToSiteHome(); + } + + return; + } + + if (data.redirect && !data.redirect.match(/^https?:\/\//)) { + // Redirect is a relative URL. Append the site URL. + data.redirect = CoreTextUtils.concatenatePaths(data.siteUrl, data.redirect); + } + + let siteIds = [siteId]; + + if (!siteId) { + // No site created, check if the site is stored (to know which one to use). + siteIds = await CoreSites.getSiteIdsFromUrl(data.siteUrl, true, data.username); + } + + if (siteIds.length > 1) { + // More than one site to treat the URL, let the user choose. + CoreContentLinksHelper.goToChooseSite(data.redirect || data.siteUrl); + + } else if (siteIds.length == 1) { + // Only one site, handle the link. + const site = await CoreSites.getSite(siteIds[0]); + + if (!data.redirect) { + // No redirect, go to the root URL if needed. + await CoreContentLinksHelper.handleRootURL(site, false, true); + } else { + // Handle the redirect link. + modal.dismiss(); // Dismiss modal so it doesn't collide with confirms. + + /* Always use the username from the site in this case. If the link has a username and a token, + this will make sure that the link is opened with the user the token belongs to. */ + const username = site.getInfo()?.username || data.username; + + const treated = await CoreContentLinksHelper.handleLink(data.redirect, username); + + if (!treated) { + CoreDomUtils.showErrorModal('core.contentlinks.errornoactions', true); + } + } + + } else { + // Site not stored. Try to add the site. + const result = await CoreSites.checkSite(data.siteUrl); + + // Site exists. We'll allow to add it. + modal.dismiss(); // Dismiss modal so it doesn't collide with confirms. + + await this.goToAddSite(data, result); + } + + } catch (error) { + throw new CoreCustomURLSchemesHandleError(error, data); + } finally { + modal.dismiss(); + + if (data.isSSOToken) { + CoreApp.finishSSOAuthentication(); + } + } + } + + /** + * Get the data from a custom URL scheme. The structure of the URL is: + * moodlemobile://username@domain.com?token=TOKEN&privatetoken=PRIVATETOKEN&redirect=http://domain.com/course/view.php?id=2 + * + * @param url URL to treat. + * @return Promise resolved with the data. + */ + protected async getCustomURLData(url: string): Promise { + if (!this.isCustomURL(url)) { + throw new CoreCustomURLSchemesHandleError(null); + } + + // App opened using custom URL scheme. + this.logger.debug('Treating custom URL scheme: ' + url); + + // Delete the sso scheme from the URL. + url = this.removeCustomURLScheme(url); + + // Detect if there's a user specified. + const username = CoreUrlUtils.getUsernameFromUrl(url); + if (username) { + url = url.replace(username + '@', ''); // Remove the username from the URL. + } + + // Get the params of the URL. + const params = CoreUrlUtils.extractUrlParams(url); + + // Remove the params to get the site URL. + if (url.indexOf('?') != -1) { + url = url.substr(0, url.indexOf('?')); + } + + if (!url.match(/https?:\/\//)) { + // Url doesn't have a protocol. Check if the site is stored in the app to be able to determine the protocol. + const siteIds = await CoreSites.getSiteIdsFromUrl(url, true, username); + + if (siteIds.length) { + // There is at least 1 site with this URL. Use it to know the full URL. + const site = await CoreSites.getSite(siteIds[0]); + + url = site.getURL(); + } + } + + return { + siteUrl: url, + username: username, + token: params.token, + privateToken: params.privateToken, + redirect: params.redirect, + isAuthenticationURL: !!params.token, + }; + } + + /** + * Get the data from a "link" custom URL scheme. This kind of URL is deprecated. + * + * @param url URL to treat. + * @return Promise resolved with the data. + */ + protected async getCustomURLLinkData(url: string): Promise { + if (!this.isCustomURLLink(url)) { + throw new CoreCustomURLSchemesHandleError(null); + } + + // App opened using custom URL scheme. + this.logger.debug('Treating custom URL scheme with link param: ' + url); + + // Delete the sso scheme from the URL. + url = this.removeCustomURLLinkScheme(url); + + // Detect if there's a user specified. + const username = CoreUrlUtils.getUsernameFromUrl(url); + if (username) { + url = url.replace(username + '@', ''); // Remove the username from the URL. + } + + // First of all, check if it's the root URL of a site. + const data = await CoreSites.isStoredRootURL(url, username); + + if (data.site) { + // Root URL. + return { + siteUrl: data.site.getURL(), + username: username, + }; + + } else if (data.siteIds.length > 0) { + // Not the root URL, but at least 1 site supports the URL. Get the site URL from the list of sites. + const site = await CoreSites.getSite(data.siteIds[0]); + + return { + siteUrl: site.getURL(), + username: username, + redirect: url, + }; + + } else { + // Get the site URL. + let siteUrl = CoreContentLinksDelegate.getSiteUrl(url); + let redirect: string | undefined = url; + + if (!siteUrl) { + // Site URL not found, use the original URL since it could be the root URL of the site. + siteUrl = url; + redirect = undefined; + } + + return { + siteUrl: siteUrl, + username: username, + redirect: redirect, + }; + } + } + + /** + * Get the data from a "token" custom URL scheme. This kind of URL is deprecated. + * + * @param url URL to treat. + * @return Promise resolved with the data. + */ + protected async getCustomURLTokenData(url: string): Promise { + if (!this.isCustomURLToken(url)) { + throw new CoreCustomURLSchemesHandleError(null); + } + + if (CoreApp.isSSOAuthenticationOngoing()) { + // Authentication ongoing, probably duplicated request. + throw new CoreCustomURLSchemesHandleError('Duplicated'); + } + + // App opened using custom URL scheme. Probably an SSO authentication. + CoreApp.startSSOAuthentication(); + this.logger.debug('App launched by URL with an SSO'); + + // Delete the sso scheme from the URL. + url = this.removeCustomURLTokenScheme(url); + + // 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'); + + throw new CoreCustomURLSchemesHandleError(null); + } + + const data: CoreCustomURLSchemesParams = await CoreLoginHelper.validateBrowserSSOLogin(url); + + data.isSSOToken = true; + data.isAuthenticationURL = true; + + return data; + } + + /** + * Go to page to add a site, or open a browser if SSO. + * + * @param data URL data. + * @param checkResponse Result of checkSite. + * @return Promise resolved when done. + */ + protected async goToAddSite(data: CoreCustomURLSchemesParams, checkResponse: CoreSiteCheckResponse): Promise { + const ssoNeeded = CoreLoginHelper.isSSOLoginNeeded(checkResponse.code); + const pageParams = { + siteUrl: checkResponse.siteUrl, + username: data.username, + urlToOpen: data.redirect, + siteConfig: checkResponse.config, + }; + let hasSitePluginsLoaded = false; + + if (CoreSites.isLoggedIn()) { + // Ask the user before changing site. + await CoreDomUtils.showConfirm(Translate.instant('core.contentlinks.confirmurlothersite')); + + if (!ssoNeeded) { + hasSitePluginsLoaded = CoreSitePlugins.hasSitePluginsLoaded; + if (hasSitePluginsLoaded) { + // Store the redirect since logout will restart the app. + CoreApp.storeRedirect(CoreConstants.NO_SITE_ID, '/login/credentials', pageParams); + } + + await CoreSites.logout(); + } + } + + if (ssoNeeded) { + CoreLoginHelper.confirmAndOpenBrowserForSSOLogin( + checkResponse.siteUrl, + checkResponse.code, + checkResponse.service, + checkResponse.config?.launchurl, + ); + } else if (!hasSitePluginsLoaded) { + await CoreNavigator.navigateToLoginCredentials(pageParams); + } + } + + /** + * Check whether a URL is a custom URL scheme. + * + * @param url URL to check. + * @return Whether it's a custom URL scheme. + */ + isCustomURL(url: string): boolean { + if (!url) { + return false; + } + + return url.indexOf(CoreConstants.CONFIG.customurlscheme + '://') != -1; + } + + /** + * Check whether a URL is a custom URL scheme with the "link" param (deprecated). + * + * @param url URL to check. + * @return Whether it's a custom URL scheme. + */ + isCustomURLLink(url: string): boolean { + if (!url) { + return false; + } + + return url.indexOf(CoreConstants.CONFIG.customurlscheme + '://link=') != -1; + } + + /** + * Check whether a URL is a custom URL scheme with a "token" param (deprecated). + * + * @param url URL to check. + * @return Whether it's a custom URL scheme. + */ + isCustomURLToken(url: string): boolean { + if (!url) { + return false; + } + + return url.indexOf(CoreConstants.CONFIG.customurlscheme + '://token=') != -1; + } + + /** + * Remove the scheme from a custom URL. + * + * @param url URL to treat. + * @return URL without scheme. + */ + removeCustomURLScheme(url: string): string { + return url.replace(CoreConstants.CONFIG.customurlscheme + '://', ''); + } + + /** + * Remove the scheme and the "link=" prefix from a link custom URL. + * + * @param url URL to treat. + * @return URL without scheme and prefix. + */ + removeCustomURLLinkScheme(url: string): string { + return url.replace(CoreConstants.CONFIG.customurlscheme + '://link=', ''); + } + + /** + * Remove the scheme and the "token=" prefix from a token custom URL. + * + * @param url URL to treat. + * @return URL without scheme and prefix. + */ + removeCustomURLTokenScheme(url: string): string { + return url.replace(CoreConstants.CONFIG.customurlscheme + '://token=', ''); + } + + /** + * Treat error returned by handleCustomURL. + * + * @param error Error data. + */ + treatHandleCustomURLError(error: CoreCustomURLSchemesHandleError): void { + if (error.error == 'Duplicated') { + // Duplicated request + } else if (CoreUtils.isWebServiceError(error.error) && error.data && error.data.isSSOToken) { + // An error occurred, display the error and logout the user. + CoreLoginHelper.treatUserTokenError(error.data.siteUrl, error.error); + CoreSites.logout(); + } else { + CoreDomUtils.showErrorModalDefault(error.error, Translate.instant('core.login.invalidsite')); + } + } + +} + +/** + * Error returned by handleCustomURL. + */ +export class CoreCustomURLSchemesHandleError extends CoreError { + + /** + * Constructor. + * + * @param error The error message or object. + * @param data Data obtained from the URL (if any). + */ + constructor(public error: string | CoreError | CoreTextErrorObject | null, public data?: CoreCustomURLSchemesParams) { + super(CoreTextUtils.getErrorMessageFromError(error)); + } + +} + +export const CoreCustomURLSchemes = makeSingleton(CoreCustomURLSchemesProvider); + +/** + * All params that can be in a custom URL scheme. + */ +export interface CoreCustomURLSchemesParams extends CoreLoginSSOData { + + /** + * Username. + */ + username?: string; + + /** + * URL to open once authenticated. + */ + redirect?: string; + + /** + * Whether it's an SSO token URL. + */ + isSSOToken?: boolean; + + /** + * Whether the URL is meant to perform an authentication. + */ + isAuthenticationURL?: boolean; +} diff --git a/src/core/services/utils/text.ts b/src/core/services/utils/text.ts index 6b351523d..ec36410a5 100644 --- a/src/core/services/utils/text.ts +++ b/src/core/services/utils/text.ts @@ -522,10 +522,10 @@ export class CoreTextUtilsProvider { /** * Get the error message from an error object. * - * @param error Error object. + * @param error Error. * @return Error message, undefined if not found. */ - getErrorMessageFromError(error?: string | CoreError | CoreTextErrorObject): string | undefined { + getErrorMessageFromError(error?: string | CoreError | CoreTextErrorObject | null): string | undefined { if (typeof error == 'string') { return error; } @@ -534,7 +534,11 @@ export class CoreTextUtilsProvider { return error.message; } - return error && (error.message || error.error || error.content || error.body); + if (!error) { + return undefined; + } + + return error.message || error.error || error.content || error.body; } /** diff --git a/src/core/services/utils/url.ts b/src/core/services/utils/url.ts index bff2c6d1f..3aa8baf33 100644 --- a/src/core/services/utils/url.ts +++ b/src/core/services/utils/url.ts @@ -362,7 +362,7 @@ export class CoreUrlUtilsProvider { * @param url URL to treat. * @return Username. Undefined if no username found. */ - getUsernameFromUrl(url: string): string | void { + getUsernameFromUrl(url: string): string | undefined { if (url.indexOf('@') > -1) { // Get URL without protocol. const withoutProtocol = url.replace(/^[^?@/]*:\/\//, ''); diff --git a/src/core/singletons/events.ts b/src/core/singletons/events.ts index cc311602e..849475ae4 100644 --- a/src/core/singletons/events.ts +++ b/src/core/singletons/events.ts @@ -45,6 +45,7 @@ export interface CoreEventsData { [CoreEvents.COMPLETION_MODULE_VIEWED]: CoreEventCompletionModuleViewedData; [CoreEvents.SECTION_STATUS_CHANGED]: CoreEventSectionStatusChangedData; [CoreEvents.ACTIVITY_DATA_SENT]: CoreEventActivityDataSentData; + [CoreEvents.IAB_LOAD_START]: InAppBrowserEvent; }; /*