diff --git a/scripts/langindex.json b/scripts/langindex.json index 8ee4c31b7..03b75f59e 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1471,9 +1471,6 @@ "core.calculating": "local_moodlemobileapp", "core.cancel": "moodle", "core.cannotconnect": "local_moodlemobileapp", - "core.cannotconnecttrouble": "local_moodlemobileapp", - "core.cannotconnecttroublewithoutsupport": "local_moodlemobileapp", - "core.cannotconnectverify": "local_moodlemobileapp", "core.cannotdownloadfiles": "local_moodlemobileapp", "core.cannotinstallapk": "local_moodlemobileapp", "core.cannotlogoutpageblocks": "local_moodlemobileapp", @@ -1520,6 +1517,7 @@ "core.confirmleaveunknownchanges": "local_moodlemobileapp", "core.confirmloss": "local_moodlemobileapp", "core.confirmopeninbrowser": "local_moodlemobileapp", + "core.connectionlost": "local_moodlemobileapp", "core.considereddigitalminor": "moodle", "core.contactsupport": "local_moodlemobileapp", "core.content": "moodle", @@ -2273,6 +2271,9 @@ "core.sitehome.sitehome": "moodle", "core.sitehome.sitenews": "moodle", "core.sitemaintenance": "admin", + "core.sitenotfound": "local_moodlemobileapp", + "core.sitenotfoundhelp": "local_moodlemobileapp", + "core.siteunavailablehelp": "local_moodlemobileapp", "core.size": "moodle", "core.sizeb": "moodle", "core.sizegb": "moodle", diff --git a/src/addons/mod/quiz/services/quiz-sync.ts b/src/addons/mod/quiz/services/quiz-sync.ts index 62904829f..1dcacd5c1 100644 --- a/src/addons/mod/quiz/services/quiz-sync.ts +++ b/src/addons/mod/quiz/services/quiz-sync.ts @@ -15,6 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; +import { CoreSite } from '@classes/site'; import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync'; import { CoreCourse, CoreCourseModuleBasicInfo } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; @@ -313,7 +314,7 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider if (!CoreNetwork.isOnline()) { // Cannot sync in offline. - throw new CoreError(Translate.instant('core.cannotconnect')); + throw new CoreError(Translate.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION })); } const offlineAttempt = offlineAttempts.pop()!; diff --git a/src/core/classes/errors/loginerror.ts b/src/core/classes/errors/loginerror.ts index 7b92853af..f77688b88 100644 --- a/src/core/classes/errors/loginerror.ts +++ b/src/core/classes/errors/loginerror.ts @@ -19,12 +19,14 @@ import { CoreSiteError, CoreSiteErrorOptions } from '@classes/errors/siteerror'; */ export class CoreLoginError extends CoreSiteError { + title?: string; critical?: boolean; loggedOut?: boolean; constructor(options: CoreLoginErrorOptions) { super(options); + this.title = options.title; this.critical = options.critical; this.loggedOut = options.loggedOut; } @@ -32,6 +34,7 @@ export class CoreLoginError extends CoreSiteError { } export type CoreLoginErrorOptions = CoreSiteErrorOptions & { + title?: string; // Error title. critical?: boolean; // Whether the error is important enough to abort the operation. loggedOut?: boolean; // Whether site has been marked as logged out. }; diff --git a/src/core/classes/errors/siteerror.ts b/src/core/classes/errors/siteerror.ts index 4b5e7035c..bb31c41b6 100644 --- a/src/core/classes/errors/siteerror.ts +++ b/src/core/classes/errors/siteerror.ts @@ -25,7 +25,7 @@ export class CoreSiteError extends CoreError { supportConfig?: CoreUserSupportConfig; constructor(options: CoreSiteErrorOptions) { - super(getErrorMessage(options)); + super(options.message); this.errorcode = options.errorcode; this.errorDetails = options.errorDetails; @@ -34,23 +34,8 @@ export class CoreSiteError extends CoreError { } -/** - * Get message to use in the error. - * - * @param options Error options. - * @returns Error message. - */ -function getErrorMessage(options: CoreSiteErrorOptions): string { - if ('supportConfig' in options && !options.supportConfig?.canContactSupport()) { - return options.fallbackMessage ?? options.message; - } - - return options.message; -} - export type CoreSiteErrorOptions = { message: string; - fallbackMessage?: string; // Message to use when contacting support is not possible but warranted. errorcode?: string; // Technical error code useful for technical assistance. errorDetails?: string; // Technical error details useful for technical assistance. diff --git a/src/core/classes/site.ts b/src/core/classes/site.ts index cd0fec4c7..ed6ee34c6 100644 --- a/src/core/classes/site.ts +++ b/src/core/classes/site.ts @@ -810,9 +810,7 @@ export class CoreSite { ): Promise { if (preSets.forceOffline) { // Don't call the WS, just fail. - throw new CoreError( - Translate.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION }), - ); + throw new CoreError(Translate.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION })); } try { @@ -1134,8 +1132,7 @@ export class CoreSite { if (!data || !data.responses) { throw new CoreSiteError({ supportConfig: new CoreUserAuthenticatedSupportConfig(this), - message: Translate.instant('core.cannotconnecttrouble'), - fallbackMessage: Translate.instant('core.cannotconnecttroublewithoutsupport'), + message: Translate.instant('core.siteunavailablehelp', { site: this.siteUrl }), errorcode: 'invalidresponse', errorDetails: Translate.instant('core.errorinvalidresponse', { method: 'tool_mobile_call_external_functions' }), }); @@ -1726,9 +1723,7 @@ export class CoreSite { .catch(async () => { if (cachePreSets.forceOffline) { // Don't call the WS, just fail. - throw new CoreError( - Translate.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION }), - ); + throw new CoreError(Translate.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION })); } // Call the WS. diff --git a/src/core/features/login/pages/site/site.ts b/src/core/features/login/pages/site/site.ts index f4fa16765..864283665 100644 --- a/src/core/features/login/pages/site/site.ts +++ b/src/core/features/login/pages/site/site.ts @@ -27,7 +27,6 @@ import { CoreLoginSiteFinderSettings, CoreLoginSiteSelectorListMethod, } from '@features/login/services/login-helper'; -import { CoreSite } from '@classes/site'; import { CoreError } from '@classes/errors/error'; import { CoreConstants } from '@/core/constants'; import { Translate } from '@singletons'; @@ -45,6 +44,8 @@ import { CoreUserSupport } from '@features/user/services/support'; import { CoreErrorInfoComponent } from '@components/error-info/error-info'; import { CoreUserSupportConfig } from '@features/user/classes/support/support-config'; import { CoreUserGuestSupportConfig } from '@features/user/classes/support/guest-support-config'; +import { CoreLoginError } from '@classes/errors/loginerror'; +import { CoreSite } from '@classes/site'; /** * Site (url) chooser when adding a new site. @@ -389,6 +390,7 @@ export class CoreLoginSitePage implements OnInit { let errorMessage = CoreDomUtils.getErrorMessage(error); let siteExists = false; let supportConfig: CoreUserSupportConfig | undefined = undefined; + let errorTitle: string | undefined; let errorDetails: string | undefined; let errorCode: string | undefined; @@ -399,32 +401,21 @@ export class CoreLoginSitePage implements OnInit { siteExists = supportConfig instanceof CoreUserGuestSupportConfig; } - if ( - !siteExists && ( - errorMessage === Translate.instant('core.cannotconnecttrouble') || - errorMessage === Translate.instant('core.cannotconnecttroublewithoutsupport') - ) - ) { - const found = this.sites.find((site) => site.url == url); - - if (!found) { - errorMessage += ' ' + Translate.instant('core.cannotconnectverify'); - } - } - - errorMessage = '

' + errorMessage + '

'; - if (!siteExists && url) { - const fullUrl = CoreUrlUtils.isAbsoluteURL(url) ? url : 'https://' + url; - errorMessage += '

' + url + '

'; + if (error instanceof CoreLoginError) { + errorTitle = error.title; } if (errorDetails) { - errorMessage += '
'; + errorMessage = `

${errorMessage}

`; } const alertSupportConfig = supportConfig; - const buttons: AlertButton[] = [ - alertSupportConfig + const buttons = [ + { + text: Translate.instant('core.tryagain'), + role: 'cancel', + }, + alertSupportConfig?.canContactSupport() ? { text: Translate.instant('core.contactsupport'), handler: () => CoreUserSupport.contact({ @@ -433,22 +424,26 @@ export class CoreLoginSitePage implements OnInit { message: `Error: ${errorCode}\n\n${errorDetails}`, }), } - : { - text: Translate.instant('core.needhelp'), - cssClass: 'core-login-need-help', - handler: () => this.showHelp(), - }, - { - text: Translate.instant('core.tryagain'), - role: 'cancel', - }, - ]; + : ( + !siteExists + ? { + text: Translate.instant('core.needhelp'), + cssClass: 'core-login-need-help', + handler: () => this.showHelp(), + } + : null + ), + ].filter(button => !!button); // @TODO: Remove CoreSite.MINIMUM_MOODLE_VERSION, not used on translations since 3.9.0. const alertElement = await CoreDomUtils.showAlertWithOptions({ - header: Translate.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION }), - message: errorMessage, - buttons, + header: errorTitle ?? ( + siteExists + ? Translate.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION }) + : Translate.instant('core.sitenotfound') + ), + message: errorMessage ?? Translate.instant('core.sitenotfoundhelp'), + buttons: buttons as AlertButton[], }); if (errorDetails) { diff --git a/src/core/features/login/tests/behat/basic_usage.feature b/src/core/features/login/tests/behat/basic_usage.feature index 1f9d41c9a..5dbea5ed4 100755 --- a/src/core/features/login/tests/behat/basic_usage.feature +++ b/src/core/features/login/tests/behat/basic_usage.feature @@ -39,14 +39,10 @@ Feature: Test basic usage of login in app But I should not find "Log in" in the app Scenario: Add a non existing account - When I enter the app - And I log in as "student1" - When I log out in the app - And I press "Add" in the app - And I set the field "Your site" to "Wrong Site Address" in the app - And I press enter in the app - Then I should find "Cannot connect" in the app - And I should find "Wrong Site Address" in the app + When I launch the app + And I set the field "Your site" to "wrongsiteaddress" in the app + And I press "Connect to your site" in the app + Then I should find "Site not found" in the app Scenario: Add a non existing account from accounts switcher When I enter the app @@ -55,10 +51,9 @@ Feature: Test basic usage of login in app And I press "Switch account" in the app And I press "Add" in the app And I wait the app to restart - And I set the field "Your site" to "Wrong Site Address" in the app - And I press enter in the app - Then I should find "Cannot connect" in the app - And I should find "Wrong Site Address" in the app + And I set the field "Your site" to "wrongsiteaddress" in the app + And I press "Connect to your site" in the app + Then I should find "Site not found" in the app Scenario: Log out from the app Given I entered the app as "student1" diff --git a/src/core/initializers/prepare-inapp-browser.ts b/src/core/initializers/prepare-inapp-browser.ts index f9631b9ed..026e6115c 100644 --- a/src/core/initializers/prepare-inapp-browser.ts +++ b/src/core/initializers/prepare-inapp-browser.ts @@ -53,10 +53,7 @@ export default function(): void { supportConfig: CoreSites.getCurrentSite() ? CoreUserAuthenticatedSupportConfig.forCurrentSite() : new CoreUserNullSupportConfig(), - message: Translate.instant('core.cannotconnecttrouble'), - fallbackMessage: Translate.instant('core.cannotconnecttroublewithoutsupport'), - errorcode: 'invalidurlscheme', - errorDetails: Translate.instant('core.errorurlschemeinvalidscheme', { $a: urlScheme }), + message: Translate.instant('core.errorurlschemeinvalidscheme', { $a: urlScheme }), })); return; diff --git a/src/core/lang.json b/src/core/lang.json index 2eb3ca8ca..58e889c0c 100644 --- a/src/core/lang.json +++ b/src/core/lang.json @@ -14,10 +14,7 @@ "browser": "Browser", "calculating": "Calculating", "cancel": "Cancel", - "cannotconnect": "Cannot connect", - "cannotconnecttrouble": "We're having trouble connecting to your site.", - "cannotconnecttroublewithoutsupport": "We're having trouble connecting to your site, please contact your institution.", - "cannotconnectverify": "Please check the address is correct.", + "cannotconnect": "Can't connect to site", "cannotdownloadfiles": "This institution has disabled downloading files.", "cannotinstallapk": "For security reasons, you can't install unknown apps on your device from this app. Please open the file using a browser.", "cannotlogoutpageblocks": "Please save or discard your changes before continuing.", @@ -54,6 +51,7 @@ "confirmleaveunknownchanges": "Are you sure you want to leave this page? If you have unsaved changes they will be lost.", "confirmloss": "Are you sure? All changes will be lost.", "confirmopeninbrowser": "Do you want to open it in a web browser?", + "connectionlost": "Connection to site lost", "considereddigitalminor": "You are too young to create an account on this site.", "contactsupport": "Contact support", "content": "Content", @@ -282,6 +280,9 @@ "showmore": "Show more...", "site": "Site", "sitemaintenance": "The site is undergoing maintenance and is currently not available", + "sitenotfound": "Site not found", + "sitenotfoundhelp": "We can't find the site you entered. Please check for typos or try again later. If you keep seeing this message, contact your school or learning provider.", + "siteunavailablehelp": "The site \"{{site}}\" is not available right now. Please try again later or contact your school or learning provider.", "size": "Size", "sizeb": "bytes", "sizegb": "GB", diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 3c503deaf..33e7eca95 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -55,6 +55,7 @@ import { lazyMap, LazyMap } from '../utils/lazy-map'; import { asyncInstance, AsyncInstance } from '../utils/async-instance'; import { CoreText } from '@singletons/text'; import { CorePromisedValue } from '@classes/promised-value'; +import { CoreSite } from '@classes/site'; /* * Factory for handling downloading files and retrieve downloaded files. @@ -509,7 +510,7 @@ export class CoreFilepoolProvider { } else { if (!CoreNetwork.isOnline()) { // Cannot check size in offline, stop. - throw new CoreError(Translate.instant('core.cannotconnect')); + throw new CoreError(Translate.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION })); } size = await CoreWS.getRemoteFileSize(fileUrl); diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index 8672928d7..d4fba5e80 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -246,7 +246,7 @@ export class CoreSitesProvider { } else if (CoreTextUtils.getErrorMessageFromError(secondError)) { throw secondError; } else { - throw new CoreError(Translate.instant('core.cannotconnecttrouble')); + throw new CoreError(Translate.instant('core.sitenotfoundhelp')); } } } @@ -297,14 +297,14 @@ export class CoreSitesProvider { // Check that the user can authenticate. if (!config.enablewebservices) { - throw this.createCannotConnectLoginError({ + throw this.createCannotConnectLoginError(config.httpswwwroot || config.wwwroot, { supportConfig: new CoreUserGuestSupportConfig(config), errorcode: 'webservicesnotenabled', errorDetails: Translate.instant('core.login.webservicesnotenabled'), critical: true, }); } else if (!config.enablemobilewebservice) { - throw this.createCannotConnectLoginError({ + throw this.createCannotConnectLoginError(config.httpswwwroot || config.wwwroot, { supportConfig: new CoreUserGuestSupportConfig(config), errorcode: 'mobileservicesnotenabled', errorDetails: Translate.instant('core.login.mobileservicesnotenabled'), @@ -333,11 +333,12 @@ export class CoreSitesProvider { * @param options Error options. * @return Cannot connect error. */ - protected createCannotConnectLoginError(options?: Partial): CoreLoginError { + protected createCannotConnectLoginError(siteUrl: string | null, options?: Partial): CoreLoginError { return new CoreLoginError({ ...options, - message: Translate.instant('core.cannotconnecttrouble'), - fallbackMessage: Translate.instant('core.cannotconnecttroublewithoutsupport'), + message: !this.isLoggedIn() && siteUrl === null + ? Translate.instant('core.sitenotfoundhelp') + : Translate.instant('core.siteunavailablehelp', { site: siteUrl ?? this.currentSite?.siteUrl }), }); } @@ -355,7 +356,10 @@ export class CoreSitesProvider { if (error instanceof CoreAjaxError || !('errorcode' in error)) { // The WS didn't return data, probably cannot connect. return new CoreLoginError({ - message: error.message || '', + title: Translate.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION }), + message: Translate.instant('core.siteunavailablehelp', { site: siteUrl }), + errorcode: 'publicconfigfailed', + errorDetails: error.message || '', critical: false, // Allow fallback to http if siteUrl uses https. }); } @@ -363,30 +367,27 @@ export class CoreSitesProvider { // Service supported but an error happened. Return error. const options: CoreLoginErrorOptions = { critical: true, - message: error.message, + title: Translate.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION }), + message: Translate.instant('core.siteunavailablehelp', { site: siteUrl }), errorcode: error.errorcode, supportConfig: error.supportConfig, - errorDetails: error.errorDetails, + errorDetails: error.errorDetails ?? error.message, }; if (error.errorcode === 'codingerror') { // This could be caused by a redirect. Check if it's the case. const redirect = await CoreUtils.checkRedirect(siteUrl); + options.message = Translate.instant('core.siteunavailablehelp', { site: siteUrl }); + if (redirect) { - options.message = Translate.instant('core.cannotconnecttrouble'); - options.fallbackMessage = Translate.instant('core.cannotconnecttroublewithoutsupport'); options.errorcode = 'sitehasredirect'; options.errorDetails = Translate.instant('core.login.sitehasredirect'); options.critical = false; // Keep checking fallback URLs. - } else { - // We can't be sure if there is a redirect or not. Display cannot connect error. - options.message = Translate.instant('core.cannotconnecttrouble'); } } else if (error.errorcode === 'invalidrecord') { // WebService not found, site not supported. - options.message = Translate.instant('core.cannotconnecttrouble'); - options.fallbackMessage = Translate.instant('core.cannotconnecttroublewithoutsupport'); + options.message = Translate.instant('core.siteunavailablehelp', { site: siteUrl }); options.errorcode = 'invalidmoodleversion'; options.errorDetails = Translate.instant('core.login.invalidmoodleversion', { $a: CoreSite.MINIMUM_MOODLE_VERSION }); } else if (error.errorcode === 'redirecterrordetected') { @@ -413,7 +414,7 @@ export class CoreSitesProvider { data = await Http.post(siteUrl + '/login/token.php', { appsitecheck: 1 }).pipe(timeout(CoreWS.getRequestTimeout())) .toPromise(); } catch (error) { - throw this.createCannotConnectLoginError({ + throw this.createCannotConnectLoginError(null, { supportConfig: await CoreUserGuestSupportConfig.forSite(siteUrl), errorcode: 'sitecheckfailed', errorDetails: CoreDomUtils.getErrorMessage(error) ?? undefined, @@ -422,7 +423,7 @@ export class CoreSitesProvider { if (data === null) { // Cannot connect. - throw this.createCannotConnectLoginError({ + throw this.createCannotConnectLoginError(null, { supportConfig: await CoreUserGuestSupportConfig.forSite(siteUrl), errorcode: 'appsitecheckfailed', errorDetails: 'A request to /login/token.php with appsitecheck=1 returned an empty response', @@ -430,7 +431,7 @@ export class CoreSitesProvider { } if (data.errorcode && (data.errorcode == 'enablewsdescription' || data.errorcode == 'requirecorrectaccess')) { - throw this.createCannotConnectLoginError({ + throw this.createCannotConnectLoginError(siteUrl, { supportConfig: await CoreUserGuestSupportConfig.forSite(siteUrl), critical: data.errorcode == 'enablewsdescription', errorcode: data.errorcode, @@ -439,7 +440,7 @@ export class CoreSitesProvider { } if (data.error && data.error == 'Web services must be enabled in Advanced features.') { - throw this.createCannotConnectLoginError({ + throw this.createCannotConnectLoginError(siteUrl, { supportConfig: await CoreUserGuestSupportConfig.forSite(siteUrl), critical: true, errorcode: 'enablewsdescription', @@ -483,11 +484,19 @@ export class CoreSitesProvider { try { data = await Http.post(loginUrl, params).pipe(timeout(CoreWS.getRequestTimeout())).toPromise(); } catch (error) { - throw new CoreError(Translate.instant('core.cannotconnecttrouble')); + throw new CoreError( + this.isLoggedIn() + ? Translate.instant('core.siteunavailablehelp', { site: this.currentSite?.siteUrl }) + : Translate.instant('core.sitenotfoundhelp'), + ); } if (data === undefined) { - throw new CoreError(Translate.instant('core.cannotconnecttrouble')); + throw new CoreError( + this.isLoggedIn() + ? Translate.instant('core.siteunavailablehelp', { site: this.currentSite?.siteUrl }) + : Translate.instant('core.sitenotfoundhelp'), + ); } else { if (data.token !== undefined) { return { token: data.token, siteUrl, privateToken: data.privatetoken }; @@ -503,7 +512,7 @@ export class CoreSitesProvider { const redirect = await CoreUtils.checkRedirect(loginUrl); if (redirect) { - throw this.createCannotConnectLoginError({ + throw this.createCannotConnectLoginError(siteUrl, { supportConfig: await CoreUserGuestSupportConfig.forSite(siteUrl), errorcode: 'sitehasredirect', errorDetails: Translate.instant('core.login.sitehasredirect'), @@ -511,7 +520,7 @@ export class CoreSitesProvider { } } - throw this.createCannotConnectLoginError({ + throw this.createCannotConnectLoginError(siteUrl, { supportConfig: await CoreUserGuestSupportConfig.forSite(siteUrl), errorcode: data.errorcode, errorDetails: data.error, diff --git a/src/core/services/tests/utils/dom.test.ts b/src/core/services/tests/utils/dom.test.ts new file mode 100644 index 000000000..3be772386 --- /dev/null +++ b/src/core/services/tests/utils/dom.test.ts @@ -0,0 +1,59 @@ +// (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 { CoreDomUtilsProvider } from '@services/utils/dom'; +import { AlertController, Translate } from '@singletons'; + +import { mock, mockSingleton, mockTranslate } from '@/testing/utils'; +import { CoreSiteError } from '@classes/errors/siteerror'; +import { CoreSites } from '@services/sites'; + +describe('CoreDomUtilsProvider', () => { + + let domUtils: CoreDomUtilsProvider; + + beforeEach(() => { + domUtils = new CoreDomUtilsProvider(); + }); + + it('shows site unavailable errors', async () => { + // Arrange. + mockTranslate({ + 'core.siteunavailablehelp': 'The site "{{site}}" is not available right now.', + }); + + const message = Translate.instant('core.siteunavailablehelp', { site: 'https://campus.example.edu' }); + const mockAlert = mock({ + present: () => Promise.resolve(), + onDidDismiss: async () => new Promise(() => { + // Never resolve. + }), + }); + + mockSingleton(AlertController, mock({ create: () => Promise.resolve(mockAlert) })); + mockSingleton(CoreSites, mock({ isLoggedIn: () => true })); + + // Act. + await domUtils.showErrorModal(new CoreSiteError({ message })); + + // Assert. + expect(mockAlert.present).toHaveBeenCalled(); + expect(AlertController.create).toHaveBeenCalledWith({ + message, + header: Translate.instant('core.connectionlost'), + buttons: [Translate.instant('core.ok')], + }); + }); + +}); diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 2401f0704..faddf4024 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -57,6 +57,7 @@ import { CoreNetwork } from '@services/network'; import { CoreSiteError } from '@classes/errors/siteerror'; import { CoreUserSupport } from '@features/user/services/support'; import { CoreErrorInfoComponent } from '@components/error-info/error-info'; +import { CoreSite } from '@classes/site'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -576,6 +577,20 @@ export class CoreDomUtilsProvider { error instanceof CoreNetworkError; } + /** + * Given a message, check if it's a site unavailable error. + * + * @param message Message text. + * @returns Whether the message is a site unavailable error. + */ + protected isSiteUnavailableError(message: string): boolean { + let siteUnavailableMessage = Translate.instant('core.siteunavailablehelp', { site: 'SITEURLPLACEHOLDER' }); + siteUnavailableMessage = CoreTextUtils.escapeForRegex(siteUnavailableMessage); + siteUnavailableMessage = siteUnavailableMessage.replace('SITEURLPLACEHOLDER', '.*'); + + return new RegExp(siteUnavailableMessage).test(message); + } + /** * Get the error message from an error, including debug data if needed. * @@ -1345,14 +1360,20 @@ export class CoreDomUtilsProvider { return null; } - const alertOptions: AlertOptions = { - message: message, - }; + const alertOptions: AlertOptions = { message }; if (this.isNetworkError(message, error)) { alertOptions.cssClass = 'core-alert-network-error'; - } else if (typeof error !== 'string' && 'title' in error) { + } + + if (typeof error !== 'string' && 'title' in error && error.title) { alertOptions.header = error.title || undefined; + } else if (message === Translate.instant('core.sitenotfoundhelp')) { + alertOptions.header = Translate.instant('core.sitenotfound'); + } else if (this.isSiteUnavailableError(message)) { + alertOptions.header = CoreSites.isLoggedIn() + ? Translate.instant('core.connectionlost') + : Translate.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION }); } else { alertOptions.header = Translate.instant('core.error'); } @@ -1360,13 +1381,14 @@ export class CoreDomUtilsProvider { if (typeof error !== 'string' && 'buttons' in error && typeof error.buttons !== 'undefined') { alertOptions.buttons = error.buttons; } else if (error instanceof CoreSiteError) { - alertOptions.buttons = []; - if (error.errorDetails) { - alertOptions.message += '
'; + alertOptions.message = `

${alertOptions.message}

`; } const supportConfig = error.supportConfig; + + alertOptions.buttons = [Translate.instant('core.ok')]; + if (supportConfig?.canContactSupport()) { alertOptions.buttons.push({ text: Translate.instant('core.contactsupport'), @@ -1377,8 +1399,6 @@ export class CoreDomUtilsProvider { }), }); } - - alertOptions.buttons.push(Translate.instant('core.ok')); } else { alertOptions.buttons = [Translate.instant('core.ok')]; } diff --git a/src/core/services/ws.ts b/src/core/services/ws.ts index adcfe5bdf..887473db0 100644 --- a/src/core/services/ws.ts +++ b/src/core/services/ws.ts @@ -41,6 +41,7 @@ import { CorePromisedValue } from '@classes/promised-value'; import { CorePlatform } from '@services/platform'; import { CoreSiteError, CoreSiteErrorOptions } from '@classes/errors/siteerror'; import { CoreUserGuestSupportConfig } from '@features/user/classes/support/guest-support-config'; +import { CoreSites } from '@services/sites'; /** * This service allows performing WS calls and download/upload files. @@ -469,10 +470,13 @@ export class CoreWSProvider { // Check if error. Ajax layer should always return an object (if error) or an array (if success). if (!data || typeof data != 'object') { + const message = CoreSites.isLoggedIn() + ? Translate.instant('core.siteunavailablehelp', { site: CoreSites.getCurrentSite()?.siteUrl }) + : Translate.instant('core.sitenotfoundhelp'); + throw new CoreAjaxError({ + message, supportConfig: await CoreUserGuestSupportConfig.forSite(preSets.siteUrl), - message: Translate.instant('core.cannotconnecttrouble'), - fallbackMessage: Translate.instant('core.cannotconnecttroublewithoutsupport'), errorcode: 'invalidresponse', errorDetails: Translate.instant('core.serverconnection', { details: Translate.instant('core.errorinvalidresponse', { method }), @@ -491,10 +495,13 @@ export class CoreWSProvider { return data.data; }, async (data: HttpErrorResponse) => { + const message = CoreSites.isLoggedIn() + ? Translate.instant('core.siteunavailablehelp', { site: CoreSites.getCurrentSite()?.siteUrl }) + : Translate.instant('core.sitenotfoundhelp'); + const options: CoreSiteErrorOptions = { + message, supportConfig: await CoreUserGuestSupportConfig.forSite(preSets.siteUrl), - message: Translate.instant('core.cannotconnecttrouble'), - fallbackMessage: Translate.instant('core.cannotconnecttroublewithoutsupport'), }; switch (data.status) { @@ -988,7 +995,9 @@ export class CoreWSProvider { */ protected createHttpError(error: CoreTextErrorObject, status: number): CoreHttpError { const message = CoreTextUtils.buildSeveralParagraphsMessage([ - Translate.instant('core.cannotconnecttrouble'), + CoreSites.isLoggedIn() + ? Translate.instant('core.siteunavailablehelp', { site: CoreSites.getCurrentSite()?.siteUrl }) + : Translate.instant('core.sitenotfoundhelp'), CoreTextUtils.getHTMLBodyContent(CoreTextUtils.getErrorMessageFromError(error) || ''), ]); @@ -1130,8 +1139,9 @@ export class CoreWSProvider { return new CoreSiteError({ ...options, supportConfig: await CoreUserGuestSupportConfig.forSite(siteUrl), - message: Translate.instant('core.cannotconnecttrouble'), - fallbackMessage: Translate.instant('core.cannotconnecttroublewithoutsupport'), + message: CoreSites.isLoggedIn() + ? Translate.instant('core.siteunavailablehelp', { site: CoreSites.getCurrentSite()?.siteUrl }) + : Translate.instant('core.sitenotfoundhelp'), }); } diff --git a/src/testing/utils.ts b/src/testing/utils.ts index 52914a3cb..18acf31a3 100644 --- a/src/testing/utils.ts +++ b/src/testing/utils.ts @@ -392,8 +392,15 @@ export function wait(time: number): Promise { */ export function mockTranslate(translations: Record = {}): void { mockSingleton(Translate as CoreSingletonProxy, { - instant: (key) => Array.isArray(key) - ? key.map(k => translations[k] ?? k) - : translations[key] ?? key, + instant: (key, replacements) => { + const applyReplacements = (text: string): string => Object.entries(replacements ?? {}).reduce( + (text, [name, value]) => text.replace(`{{${name}}}`, value), + text, + ); + + return Array.isArray(key) + ? key.map(k => applyReplacements(translations[k] ?? k)) + : applyReplacements(translations[key] ?? key); + }, }); }