diff --git a/src/app/app.scss b/src/app/app.scss index a75d033d7..b384030c3 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -46,19 +46,6 @@ ion-app.app-root { text-transform: none; } - @include media-breakpoint-up(sm) { - .core-center-view .scroll-content { - display: flex!important; - align-content: center !important; - align-items: center !important; - > * { - margin: 0 auto; - width: 100%; - max-width: 600px; - } - } - } - @include media-breakpoint-down(sm) { .hidden-phone { display: none !important; diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 94b4a81ad..7f19738e8 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1332,7 +1332,9 @@ "core.block.blocks": "Blocks", "core.browser": "Browser", "core.cancel": "Cancel", - "core.cannotconnect": "Cannot connect: Verify that you have correctly typed your site address.", + "core.cannotconnect": "Cannot connect", + "core.cannotconnecttrouble": "We're having trouble connecting to your site.", + "core.cannotconnectverify": "Please check the address is correct.", "core.cannotdownloadfiles": "File downloading is disabled. Please contact your site administrator.", "core.captureaudio": "Record audio", "core.capturedimage": "Taken picture.", @@ -1759,8 +1761,6 @@ "core.login.policyagreement": "Site policy agreement", "core.login.policyagreementclick": "Link to site policy agreement", "core.login.potentialidps": "Log in using your account on:", - "core.login.problemconnectingerror": "We're having trouble connecting to", - "core.login.problemconnectingerrorcontinue": "Double check you've entered the address correctly and try again.", "core.login.profileinvaliddata": "Invalid value", "core.login.recaptchachallengeimage": "reCAPTCHA challenge image", "core.login.recaptchaexpired": "Verification expired. Answer the security question again.", @@ -1774,7 +1774,7 @@ "core.login.selectacountry": "Select a country", "core.login.selectsite": "Please select your site:", "core.login.signupplugindisabled": "{{$a}} is not enabled.", - "core.login.siteaddress": "Your site address", + "core.login.siteaddress": "Your site", "core.login.sitehasredirect": "Your site contains at least one HTTP redirect. The app cannot follow redirects, this could be the issue that's preventing the app from connecting to your site.", "core.login.siteinmaintenance": "Your site is in maintenance mode", "core.login.sitepolicynotagreederror": "Site policy not agreed.", @@ -1788,7 +1788,8 @@ "core.login.usernamerequired": "Username required", "core.login.usernotaddederror": "User not added - error", "core.login.visitchangepassword": "Do you want to visit the site to change the password?", - "core.login.webservicesnotenabled": "Web services are not enabled in your site. Please contact your site administrator if you think they should be enabled.", + "core.login.webservicesnotenabled": "Your host site may not have enabled Web services. Please contact your administrator for help.", + "core.login.yourenteredsite": "Connect to your site", "core.lostconnection": "Your authentication token is invalid or has expired. You will have to reconnect to the site.", "core.mainmenu.changesite": "Change site", "core.mainmenu.help": "Help", diff --git a/src/core/login/lang/en.json b/src/core/login/lang/en.json index 3624b1063..7631d77bc 100644 --- a/src/core/login/lang/en.json +++ b/src/core/login/lang/en.json @@ -73,8 +73,6 @@ "policyagreement": "Site policy agreement", "policyagreementclick": "Link to site policy agreement", "potentialidps": "Log in using your account on:", - "problemconnectingerror": "We're having trouble connecting to", - "problemconnectingerrorcontinue": "Double check you've entered the address correctly and try again.", "profileinvaliddata": "Invalid value", "recaptchachallengeimage": "reCAPTCHA challenge image", "recaptchaexpired": "Verification expired. Answer the security question again.", @@ -88,7 +86,7 @@ "selectacountry": "Select a country", "selectsite": "Please select your site:", "signupplugindisabled": "{{$a}} is not enabled.", - "siteaddress": "Your site address", + "siteaddress": "Your site", "sitehasredirect": "Your site contains at least one HTTP redirect. The app cannot follow redirects, this could be the issue that's preventing the app from connecting to your site.", "siteinmaintenance": "Your site is in maintenance mode", "sitepolicynotagreederror": "Site policy not agreed.", @@ -102,5 +100,6 @@ "usernamerequired": "Username required", "usernotaddederror": "User not added - error", "visitchangepassword": "Do you want to visit the site to change the password?", - "webservicesnotenabled": "Web services are not enabled in your site. Please contact your site administrator if you think they should be enabled." + "yourenteredsite": "Connect to your site", + "webservicesnotenabled": "Your host site may not have enabled Web services. Please contact your administrator for help." } \ No newline at end of file diff --git a/src/core/login/login.scss b/src/core/login/login.scss index 7dce8dd70..2ea9290ef 100644 --- a/src/core/login/login.scss +++ b/src/core/login/login.scss @@ -1,11 +1,28 @@ +$core-login-page-background-color: $white !default; +$core-login-page-text-color: $text-color !default; +$core-login-button-outline: false !default; +$core-login-loading-color: false !default; +$core-login-item-inner-background-color: $white !default; +$core-login-item-background-color: $white !default; + +// Dark. +$core-dark-login-page-background-color: $black !default; +$core-dark-login-page-text-color: $core-dark-text-color !default; +$core-dark-login-item-inner-background-color: $core-dark-login-page-background-color !default; +$core-dark-login-item-background-color: $core-dark-login-page-background-color !default; +$core-dark-login-button-outline: $core-login-button-outline !default; +$core-dark-login-loading-color: $core-dark-text-color !default; + ion-app.app-root page-core-login-credentials, ion-app.app-root page-core-login-reconnect, ion-app.app-root page-core-login-site { .scroll-content { background: $core-login-page-background-color; + color: $core-login-page-text-color; @include darkmode() { background: $core-dark-login-page-background-color; + color: $core-dark-login-page-text-color; } } @@ -13,35 +30,18 @@ ion-app.app-root page-core-login-site { max-width: 100%; } - img.login-logo { - width: 90%; - max-width: 300px; - } - - .box { - padding: 16px; - margin: 8px; - background: $core-login-box-background-color; - border: 1px solid $core-login-box-background-border; - color: $core-login-box-text-color; - - @include darkmode() { - background: $core-dark-login-box-background-color; - border-color: $core-dark-login-box-background-border; - color: $core-dark-login-box-text-color; - } - - .item { - @include darkmode() { - background: $core-dark-login-box-background-color; - } - } - } - .core-sitename, .core-siteurl { @if $core-fixed-url { display: none; } } + .core-sitename + .core-siteurl { + margin-top: 0; + } + + .core-sitename { + font-size: 1.8rem; + } + @if $core-login-button-outline { .button-md.button-default-md, .button-ios.button-default-ios { @extend .button-md-light; @@ -77,4 +77,27 @@ ion-app.app-root page-core-login-site { .item-input { margin-bottom: 20px; } + + ion-list.core-login-forgotten-password { + margin-top: 0; + margin-bottom: 0; + + a.item { + background: transparent; + text-decoration: underline; + + @include darkmode() { + background: transparent; + } + } + } + + .core-login-site-logo { + margin-top: 5px; + margin-bottom: 5px; + img { + width: 90%; + max-width: 300px; + } + } } diff --git a/src/core/login/pages/change-password/change-password.html b/src/core/login/pages/change-password/change-password.html index 7c965a3a0..eb9f83042 100644 --- a/src/core/login/pages/change-password/change-password.html +++ b/src/core/login/pages/change-password/change-password.html @@ -8,7 +8,7 @@ - +

{{ 'core.login.forcepasswordchangenotice' | translate }}

diff --git a/src/core/login/pages/credentials/credentials.html b/src/core/login/pages/credentials/credentials.html index cff98e8b5..3bcde32fa 100644 --- a/src/core/login/pages/credentials/credentials.html +++ b/src/core/login/pages/credentials/credentials.html @@ -9,52 +9,54 @@ - + -
-
+
+ - - - - - - - +

+

{{siteUrl}}

+ + + + + + + + + diff --git a/src/core/login/pages/credentials/credentials.scss b/src/core/login/pages/credentials/credentials.scss deleted file mode 100644 index e93abd9d1..000000000 --- a/src/core/login/pages/credentials/credentials.scss +++ /dev/null @@ -1,5 +0,0 @@ -ion-app.app-root page-core-login-credentials { - .item-input { - margin-bottom: 20px; - } -} diff --git a/src/core/login/pages/credentials/credentials.ts b/src/core/login/pages/credentials/credentials.ts index 5c2d5909e..2702200bd 100644 --- a/src/core/login/pages/credentials/credentials.ts +++ b/src/core/login/pages/credentials/credentials.ts @@ -65,6 +65,8 @@ export class CoreLoginCredentialsPage { private eventsProvider: CoreEventsProvider) { this.siteUrl = navParams.get('siteUrl'); + this.siteName = navParams.get('siteName') || null; + this.logoUrl = navParams.get('logoUrl') || null; this.siteConfig = navParams.get('siteConfig'); this.urlToOpen = navParams.get('urlToOpen'); @@ -170,8 +172,6 @@ export class CoreLoginCredentialsPage { this.eventsProvider.trigger(CoreEventsProvider.LOGIN_SITE_CHECKED, { config: this.siteConfig }); } } else { - this.siteName = null; - this.logoUrl = null; this.authInstructions = null; this.canSignup = false; this.identityProviders = []; diff --git a/src/core/login/pages/reconnect/reconnect.html b/src/core/login/pages/reconnect/reconnect.html index 1b87186ee..986354d87 100644 --- a/src/core/login/pages/reconnect/reconnect.html +++ b/src/core/login/pages/reconnect/reconnect.html @@ -3,72 +3,69 @@ {{ 'core.login.reconnect' | translate }} - -
-
- - - - {{ 'core.pictureof' | translate:{$a: site.fullname} }} - - + +
+ + + + {{ 'core.pictureof' | translate:{$a: site.fullname} }} + + - - - - - - - -

{{siteUrl}}

- -

-

{{siteUrl}}

- -

- {{ 'core.login.reconnectdescription' | translate }} -

-
- - - - + + + + + + + + + + + + {{ 'core.login.cancel' | translate }} + +
diff --git a/src/core/login/pages/reconnect/reconnect.scss b/src/core/login/pages/reconnect/reconnect.scss index a28e78d05..f6012b95b 100644 --- a/src/core/login/pages/reconnect/reconnect.scss +++ b/src/core/login/pages/reconnect/reconnect.scss @@ -30,7 +30,7 @@ ion-app.app-root page-core-login-reconnect { } } - .item-input { - margin-bottom: 20px; + .core-login-reconnect-warning { + color: $red; } } diff --git a/src/core/login/pages/site-error/site-error.html b/src/core/login/pages/site-error/site-error.html deleted file mode 100644 index 8c42122fc..000000000 --- a/src/core/login/pages/site-error/site-error.html +++ /dev/null @@ -1,27 +0,0 @@ - - - {{ 'core.error' | translate }} - - - - - - - -

{{ 'core.whoops' | translate }}

-

{{ 'core.login.problemconnectingerror' | translate }}

-

{{siteUrl}}

-

{{ 'core.login.problemconnectingerrorcontinue' | translate }}

- -

{{ 'core.login.stillcantconnect' | translate }}

-

{{ 'core.login.contactyouradministrator' | translate }}

-

- {{ 'core.login.contactyouradministratorissue' | translate:{$a: ''} }} -

-

- -

-
- diff --git a/src/core/login/pages/site-error/site-error.module.ts b/src/core/login/pages/site-error/site-error.module.ts deleted file mode 100644 index 73332b1e1..000000000 --- a/src/core/login/pages/site-error/site-error.module.ts +++ /dev/null @@ -1,31 +0,0 @@ -// (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 { NgModule } from '@angular/core'; -import { IonicPageModule } from 'ionic-angular'; -import { CoreLoginSiteErrorPage } from './site-error'; -import { TranslateModule } from '@ngx-translate/core'; -import { CoreDirectivesModule } from '@directives/directives.module'; - -@NgModule({ - declarations: [ - CoreLoginSiteErrorPage - ], - imports: [ - CoreDirectivesModule, - IonicPageModule.forChild(CoreLoginSiteErrorPage), - TranslateModule.forChild() - ] -}) -export class CoreLoginSiteErrorPageModule {} diff --git a/src/core/login/pages/site-error/site-error.scss b/src/core/login/pages/site-error/site-error.scss deleted file mode 100644 index 510539e27..000000000 --- a/src/core/login/pages/site-error/site-error.scss +++ /dev/null @@ -1,3 +0,0 @@ - page-core-login-site-error button.button.button-block { - margin-bottom: 3rem; - } \ No newline at end of file diff --git a/src/core/login/pages/site-error/site-error.ts b/src/core/login/pages/site-error/site-error.ts deleted file mode 100644 index 5d4df0e53..000000000 --- a/src/core/login/pages/site-error/site-error.ts +++ /dev/null @@ -1,41 +0,0 @@ -// (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 { Component } from '@angular/core'; -import { IonicPage, ViewController, NavParams } from 'ionic-angular'; - -/** - * Component that displays an error when trying to connect to a site. - */ -@IonicPage({ segment: 'core-login-site-error' }) -@Component({ - selector: 'page-core-login-site-error', - templateUrl: 'site-error.html', -}) -export class CoreLoginSiteErrorPage { - siteUrl: string; - issue: string; - - constructor(private viewCtrl: ViewController, params: NavParams) { - this.siteUrl = params.get('siteUrl'); - this.issue = params.get('issue'); - } - - /** - * Close modal. - */ - closeModal(): void { - this.viewCtrl.dismiss(); - } -} diff --git a/src/core/login/pages/site/site.html b/src/core/login/pages/site/site.html index 6d13b7507..9a4fe01bc 100644 --- a/src/core/login/pages/site/site.html +++ b/src/core/login/pages/site/site.html @@ -9,9 +9,9 @@ - -
-
+ +
+
@@ -19,19 +19,36 @@

{{ 'core.login.siteaddress' | translate }}

- +
+ + + + + {{ 'core.login.selectsite' | translate }} {{site.name}} - -
+

{{ 'core.login.selectsite' | translate }}

@@ -48,33 +65,9 @@ {{site.name}}
- -
- - - {{ 'core.whoops' | translate }} - - -

- -

{{ 'core.login.problemconnectingerror' | translate }}

-

{{ error.url }}

-

{{ 'core.login.problemconnectingerrorcontinue' | translate }}

-
-
- - {{ 'core.login.stillcantconnect' | translate }} - - -

{{ 'core.login.contactyouradministrator' | translate }}

-

{{ 'core.whoissiteadmin' | translate }}

-
-
-
- - - diff --git a/src/core/login/pages/site/site.scss b/src/core/login/pages/site/site.scss index 0a740cead..31fcbd528 100644 --- a/src/core/login/pages/site/site.scss +++ b/src/core/login/pages/site/site.scss @@ -11,23 +11,108 @@ ion-app.app-root page-core-login-site { } } - .core-site-error { - background: $red-light; - margin-left: 0; - margin-right: 0; - width: 100%; - user-select: text; - - p, ion-card-header { - color: $red-dark; - user-select: text; - } - ion-card-header { - font-weight: bold; - } - } - - .core-login-need-help { + .core-login-need-help.item { + background: transparent; text-decoration: underline; + + @include darkmode() { + background: transparent; + } } -} \ No newline at end of file + + .core-login-site-connect { + margin-top: 1.4rem; + } + + .item ion-thumbnail { + min-width: 50px; + min-height: 50px; + border-radius: 20%; + box-shadow: 0 0 4px #eee; + text-align: center; + + img { + width: 50px; + height: 50px; + } + ion-icon { + margin: 0 auto; + font-size: 40px; + line-height: 50px; + } + } + + .core-login-site-logo, + .core-login-site-list { + transition-delay: 0s; + visibility: visible; + opacity: 1; + transition: all 0.7s ease-in-out; + max-height: 9999px; + + &.hidden { + opacity: 0; + visibility: hidden; + margin: 0; + padding: 0; + max-height: 0; + } + } + + .core-login-site-list.dimmed { + pointer-events: none; + position: relative; + } + + .core-login-site-list-loading { + position: absolute; + @include position(0, 0, 0, 0); + width: 100%; + height: 100%; + display: flex; + align-content: center; + align-items: center; + background-color: rgba(255, 255, 255, 0.5); + z-index: 1; + ion-spinner { + flex: 1; + } + } + + .core-login-site-nolist-loading { + text-align: center; + } + + .item.core-login-site-list-title { + ion-label, ion-label h2.item-heading { + margin-top: 0; + } + } + + @include media-breakpoint-up(md) { + .scroll-content > * { + max-width: 600px; + margin: 0 auto; + width: 100%; + } + .core-login-site-logo { + margin-top: 20%; + } + + &.hidden { + margin: 0; + } + } + + .core-login-entered-site { + background-color: $gray-lighter; + ion-thumbnail { + box-shadow: 0 0 4px #ddd; + } + } + + + .core-login-default-icon { + filter: grayscale(100%); + } +} diff --git a/src/core/login/pages/site/site.ts b/src/core/login/pages/site/site.ts index 59b87f862..180e0ed47 100644 --- a/src/core/login/pages/site/site.ts +++ b/src/core/login/pages/site/site.ts @@ -13,36 +13,26 @@ // limitations under the License. import { Component, ViewChild, ElementRef } from '@angular/core'; -import { IonicPage, NavController, ModalController, NavParams } from 'ionic-angular'; +import { IonicPage, NavController, ModalController, AlertController, NavParams } from 'ionic-angular'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; -import { CoreSitesProvider, CoreSiteCheckResponse } from '@providers/sites'; +import { CoreSitesProvider, CoreSiteCheckResponse, CoreLoginSiteInfo } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreConfigConstants } from '../../../../configconstants'; import { CoreLoginHelperProvider } from '../../providers/helper'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { FormBuilder, FormGroup, ValidatorFn, AbstractControl } from '@angular/forms'; import { CoreUrl } from '@singletons/url'; import { TranslateService } from '@ngx-translate/core'; /** - * Data about an error when connecting to a site. + * Extended data for UI implementation. */ -type CoreLoginSiteError = { - /** - * The error message that ocurred. - */ - message: string; - - /** - * URL the user entered. - */ - url?: string; - - /** - * URL the user entered with protocol added if needed. - */ - fullUrl?: string; +type CoreLoginSiteInfoExtended = CoreLoginSiteInfo & { + fromWS?: boolean; // If the site came from the WS call. + noProtocolUrl?: string; // Url wihtout protocol. + country?: string; // Based on countrycode. }; /** @@ -58,12 +48,16 @@ export class CoreLoginSitePage { @ViewChild('siteFormEl') formElement: ElementRef; siteForm: FormGroup; - fixedSites: any[]; - filteredSites: any[]; + fixedSites: CoreLoginSiteInfo[]; + filteredSites: CoreLoginSiteInfo[]; fixedDisplay = 'buttons'; showKeyboard = false; filter = ''; - error: CoreLoginSiteError; + sites: CoreLoginSiteInfoExtended[] = []; + hasSites = false; + loadingSites = false; + onlyWrittenSite = false; + searchFnc: Function; constructor(navParams: NavParams, protected navCtrl: NavController, @@ -72,10 +66,12 @@ export class CoreLoginSitePage { protected sitesProvider: CoreSitesProvider, protected loginHelper: CoreLoginHelperProvider, protected modalCtrl: ModalController, + protected alertCtrl: AlertController, + protected urlUtils: CoreUrlUtilsProvider, protected domUtils: CoreDomUtilsProvider, protected eventsProvider: CoreEventsProvider, protected translate: TranslateService, - protected urlUtils: CoreUrlUtilsProvider) { + protected utils: CoreUtilsProvider) { this.showKeyboard = !!navParams.get('showKeyboard'); @@ -94,8 +90,44 @@ export class CoreLoginSitePage { } this.siteForm = fb.group({ - siteUrl: [url, Validators.required] + siteUrl: [url, this.moodleUrlValidator()] }); + + this.searchFnc = this.utils.debounce(async (search: string, isValid: boolean = false) => { + search = search.trim(); + + if (search.length >= 3) { + this.onlyWrittenSite = false; + + // Update the sites list. + this.sites = await this.sitesProvider.findSites(search); + + // UI tweaks. + this.sites.forEach((site) => { + site.noProtocolUrl = CoreUrl.removeProtocol(site.url); + site.fromWS = true; + site.country = this.utils.getCountryName(site.countrycode); + }); + + // If it's a valid URL, add it. + if (isValid) { + this.onlyWrittenSite = !!this.sites.length; + this.sites.unshift({ + url: search, + fromWS: false, + name: this.translate.instant('core.login.yourenteredsite'), + noProtocolUrl: CoreUrl.removeProtocol(search), + }); + } + + this.hasSites = !!this.sites.length; + } else { + // Not reseting the array to allow animation to be displayed. + this.hasSites = false; + } + + this.loadingSites = false; + }, 1000); } /** @@ -103,8 +135,9 @@ export class CoreLoginSitePage { * * @param e Event. * @param url The URL to connect to. + * @param foundSite The site clicked, if any, from the found sites list. */ - connect(e: Event, url: string): void { + connect(e: Event, url: string, foundSite?: CoreLoginSiteInfoExtended): void { e.preventDefault(); e.stopPropagation(); @@ -130,8 +163,6 @@ export class CoreLoginSitePage { return; } - this.hideLoginIssue(); - const modal = this.domUtils.showModalLoading(), siteData = this.sitesProvider.getDemoSiteData(url); @@ -174,7 +205,7 @@ export class CoreLoginSitePage { return Promise.reject(error); }) - .then((result) => this.login(result)) + .then((result) => this.login(result, foundSite)) .catch((error) => this.showLoginIssue(url, error)) .finally(() => modal.dismiss()); } @@ -204,13 +235,6 @@ export class CoreLoginSitePage { modal.present(); } - /** - * Hide the login error. - */ - protected hideLoginIssue(): void { - this.error = null; - } - /** * Show an error that aims people to solve the issue. * @@ -218,13 +242,60 @@ export class CoreLoginSitePage { * @param error Error to display. */ protected showLoginIssue(url: string, error: any): void { - this.error = { - url: url, - message: this.domUtils.getErrorMessage(error), - }; + error = this.domUtils.getErrorMessage(error); + if (error == this.translate.instant('core.cannotconnecttrouble')) { + const found = this.sites.find((site) => site.fromWS && site.url == url); + + if (!found) { + error += ' ' + this.translate.instant('core.cannotconnectverify'); + } + } + + let message = '

' + error + '

'; if (url) { - this.error.fullUrl = this.urlUtils.isAbsoluteURL(url) ? url : 'https://' + url; + const fullUrl = this.urlUtils.isAbsoluteURL(url) ? url : 'https://' + url; + message += '

' + url + '

'; + } + + const buttons = [ + { + text: this.translate.instant('core.needhelp'), + handler: (): void => { + this.showHelp(); + } + }, + { + text: this.translate.instant('core.tryagain'), + role: 'cancel' + } + ]; + + this.domUtils.showAlertWithButtons(this.translate.instant('core.cannotconnect'), message, buttons); + } + + /** + * Find a site on the backend. + * + * @param e Event. + * @param search Text to search. + */ + searchSite(e: Event, search: string): void { + this.loadingSites = true; + + this.searchFnc(search.trim(), this.siteForm.valid); + } + + /** + * Get the demo data for a certain "name" if it is a demo site. + * + * @param name Name of the site to check. + * @return Site data if it's a demo site, undefined otherwise. + */ + getDemoSiteData(name: string): any { + const demoSites = CoreConfigConstants.demo_sites; + if (typeof demoSites != 'undefined' && typeof demoSites[name] != 'undefined') { + return demoSites[name]; } } @@ -232,10 +303,11 @@ export class CoreLoginSitePage { * Process login to a site. * * @param response Response obtained from the site check request. + * @param foundSite The site clicked, if any, from the found sites list. * * @return Promise resolved after logging in. */ - protected async login(response: CoreSiteCheckResponse): Promise { + protected async login(response: CoreSiteCheckResponse, foundSite?: CoreLoginSiteInfoExtended): Promise { return this.sitesProvider.checkRequiredMinimumVersion(response.config).then(() => { this.domUtils.triggerFormSubmittedEvent(this.formElement, true); @@ -249,11 +321,39 @@ export class CoreLoginSitePage { this.loginHelper.confirmAndOpenBrowserForSSOLogin( response.siteUrl, response.code, response.service, response.config && response.config.launchurl); } else { - this.navCtrl.push('CoreLoginCredentialsPage', { siteUrl: response.siteUrl, siteConfig: response.config }); + const pageParams = { siteUrl: response.siteUrl, siteConfig: response.config }; + if (foundSite) { + pageParams['siteName'] = foundSite.name; + pageParams['logoUrl'] = foundSite.imageurl; + } + + this.navCtrl.push('CoreLoginCredentialsPage', pageParams); } }).catch(() => { // Ignore errors. }); } + /** + * Validate Url. + * + * @return {ValidatorFn} Validation results. + */ + protected moodleUrlValidator(): ValidatorFn { + return (control: AbstractControl): {[key: string]: any} | null => { + const value = control.value.trim(); + let valid = value.length >= 3 && CoreUrl.isValidMoodleUrl(value); + + if (!valid) { + const demo = !!this.getDemoSiteData(value); + + if (demo) { + valid = true; + } + } + + return valid ? null : {siteUrl: {value: control.value}}; + }; + } + } diff --git a/src/lang/en.json b/src/lang/en.json index 43ea6efc1..7764553bb 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -12,7 +12,9 @@ "back": "Back", "browser": "Browser", "cancel": "Cancel", - "cannotconnect": "Cannot connect: Verify that you have correctly typed your site address.", + "cannotconnect": "Cannot connect", + "cannotconnecttrouble": "We're having trouble connecting to your site.", + "cannotconnectverify": "Please check the address is correct.", "cannotdownloadfiles": "File downloading is disabled. Please contact your site administrator.", "captureaudio": "Record audio", "capturedimage": "Taken picture.", diff --git a/src/providers/sites.ts b/src/providers/sites.ts index 22d42b4e0..5eabb68c0 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -165,6 +165,41 @@ export interface CoreSiteSchema { migrate?(db: SQLiteDB, oldVersion: number, siteId: string): Promise | void; } +/** + * Data about sites to be listed. + */ +export interface CoreLoginSiteInfo { + /** + * Site name. + */ + name: string; + + /** + * Site alias. + */ + alias?: string; + + /** + * URL of the site. + */ + url: string; + + /** + * Image URL of the site. + */ + imageurl?: string; + + /** + * City of the site. + */ + city?: string; + + /** + * Countrycode of the site. + */ + countrycode?: string; +} + /** * Registered site schema. */ @@ -367,10 +402,17 @@ export class CoreSitesProvider { ] }; - constructor(logger: CoreLoggerProvider, private http: HttpClient, private sitesFactory: CoreSitesFactoryProvider, - private appProvider: CoreAppProvider, private translate: TranslateService, private urlUtils: CoreUrlUtilsProvider, - private eventsProvider: CoreEventsProvider, private textUtils: CoreTextUtilsProvider, - private utils: CoreUtilsProvider, private injector: Injector, private wsProvider: CoreWSProvider, + constructor(logger: CoreLoggerProvider, + protected http: HttpClient, + protected sitesFactory: CoreSitesFactoryProvider, + protected appProvider: CoreAppProvider, + protected translate: TranslateService, + protected urlUtils: CoreUrlUtilsProvider, + protected eventsProvider: CoreEventsProvider, + protected textUtils: CoreTextUtilsProvider, + protected utils: CoreUtilsProvider, + protected injector: Injector, + protected wsProvider: CoreWSProvider, protected domUtils: CoreDomUtilsProvider) { this.logger = logger.getInstance('CoreSitesProvider'); @@ -431,7 +473,7 @@ export class CoreSitesProvider { } else if (this.textUtils.getErrorMessageFromError(secondError)) { return Promise.reject(secondError); } else { - return this.translate.instant('core.cannotconnect', {$a: CoreSite.MINIMUM_MOODLE_VERSION}); + return this.translate.instant('core.cannotconnecttrouble'); } }); }); @@ -523,8 +565,7 @@ export class CoreSitesProvider { error.error = this.translate.instant('core.login.sitehasredirect'); } else { // We can't be sure if there is a redirect or not. Display cannot connect error. - error.error = this.translate.instant('core.cannotconnect', - {$a: CoreSite.MINIMUM_MOODLE_VERSION}); + error.error = this.translate.instant('core.cannotconnecttrouble'); } return Promise.reject(error); @@ -569,7 +610,7 @@ export class CoreSitesProvider { return this.http.post(siteUrl + '/login/token.php', {}).timeout(this.wsProvider.getRequestTimeout()).toPromise() .catch(() => { // Default error messages are kinda bad, return our own message. - return Promise.reject({error: this.translate.instant('core.cannotconnect', {$a: CoreSite.MINIMUM_MOODLE_VERSION})}); + return Promise.reject({error: this.translate.instant('core.cannotconnecttrouble')}); }).then((data: any) => { if (data === null) { @@ -616,7 +657,7 @@ export class CoreSitesProvider { return promise.then((data: any): any => { if (typeof data == 'undefined') { - return Promise.reject(this.translate.instant('core.cannotconnect', {$a: CoreSite.MINIMUM_MOODLE_VERSION})); + return Promise.reject(this.translate.instant('core.cannotconnecttrouble')); } else { if (typeof data.token != 'undefined') { return { token: data.token, siteUrl: siteUrl, privateToken: data.privatetoken }; @@ -648,7 +689,7 @@ export class CoreSitesProvider { } } }, () => { - return Promise.reject(this.translate.instant('core.cannotconnect', {$a: CoreSite.MINIMUM_MOODLE_VERSION})); + return Promise.reject(this.translate.instant('core.cannotconnecttrouble')); }); } @@ -1931,6 +1972,16 @@ export class CoreSitesProvider { return {}; } } + + /** + * Returns site info found on the backend. + * + * @param search Searched text. + * @return Site info list. + */ + async findSites(search: string): Promise { + return []; + } } export class CoreSites extends makeSingleton(CoreSitesProvider) {} diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index 743a333ec..b4c904f9a 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -15,7 +15,7 @@ import { Injectable, SimpleChange, ElementRef } from '@angular/core'; import { LoadingController, Loading, ToastController, Toast, AlertController, Alert, Platform, Content, PopoverController, - ModalController, + ModalController, AlertButton } from 'ionic-angular'; import { DomSanitizer } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; @@ -1138,65 +1138,76 @@ export class CoreDomUtilsProvider { * @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. * @return Promise resolved with the alert modal. */ - showAlert(title: string, message: string, buttonText?: string, autocloseTime?: number): Promise { + async showAlert(title: string, message: string, buttonText?: string, autocloseTime?: number): Promise { + const buttons = [buttonText || this.translate.instant('core.ok')]; + + return this.showAlertWithButtons(title, message, buttons, autocloseTime); + } + + /** + * Show an alert modal with some buttons. + * + * @param title Title to show. + * @param message Message to show. + * @param buttons Buttons objects or texts. + * @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. + * @return Promise resolved with the alert modal. + */ + async showAlertWithButtons(title: string, message: string, buttons: (string | AlertButton)[], autocloseTime?: number): + Promise { const hasHTMLTags = this.textUtils.hasHTMLTags(message); - let promise; if (hasHTMLTags) { // Format the text. - promise = this.textUtils.formatText(message); - } else { - promise = Promise.resolve(message); + message = await this.textUtils.formatText(message); } - return promise.then((message) => { - const alertId = Md5.hashAsciiStr((title || '') + '#' + (message || '')); + const alertId = Md5.hashAsciiStr((title || '') + '#' + (message || '')); - if (this.displayedAlerts[alertId]) { - // There's already an alert with the same message and title. Return it. - return this.displayedAlerts[alertId]; - } + if (this.displayedAlerts[alertId]) { + // There's already an alert with the same message and title. Return it. + return this.displayedAlerts[alertId]; + } - const alert: CoreAlert = this.alertCtrl.create({ - title: title, - message: message, - buttons: [buttonText || this.translate.instant('core.ok')] - }); - - alert.present().then(() => { - if (hasHTMLTags) { - // Treat all anchors so they don't override the app. - const alertMessageEl: HTMLElement = alert.pageRef().nativeElement.querySelector('.alert-message'); - this.treatAnchors(alertMessageEl); - } - }); - - // Store the alert and remove it when dismissed. - this.displayedAlerts[alertId] = alert; - - // Define the observables to extend the Alert class. This will allow several callbacks instead of just one. - alert.didDismiss = new Subject(); - alert.willDismiss = new Subject(); - - // Set the callbacks to trigger an observable event. - alert.onDidDismiss((data: any, role: string) => { - delete this.displayedAlerts[alertId]; - - alert.didDismiss.next({data: data, role: role}); - }); - - alert.onWillDismiss((data: any, role: string) => { - alert.willDismiss.next({data: data, role: role}); - }); - - if (autocloseTime > 0) { - setTimeout(() => { - alert.dismiss(); - }, autocloseTime); - } - - return alert; + const alert: CoreAlert = this.alertCtrl.create({ + title: title, + message: message, + buttons: buttons, }); + + alert.present().then(() => { + if (hasHTMLTags) { + // Treat all anchors so they don't override the app. + const alertMessageEl: HTMLElement = alert.pageRef().nativeElement.querySelector('.alert-message'); + this.treatAnchors(alertMessageEl); + } + }); + + // Store the alert and remove it when dismissed. + this.displayedAlerts[alertId] = alert; + + // Define the observables to extend the Alert class. This will allow several callbacks instead of just one. + alert.didDismiss = new Subject(); + alert.willDismiss = new Subject(); + + // Set the callbacks to trigger an observable event. + alert.onDidDismiss((data: any, role: string) => { + delete this.displayedAlerts[alertId]; + + alert.didDismiss.next({data: data, role: role}); + }); + + alert.onWillDismiss((data: any, role: string) => { + alert.willDismiss.next({data: data, role: role}); + }); + + if (autocloseTime > 0) { + setTimeout(() => { + alert.dismiss(); + }, autocloseTime); + } + + return alert; } /** diff --git a/src/providers/utils/url.ts b/src/providers/utils/url.ts index f9b07929b..7700b6e18 100644 --- a/src/providers/utils/url.ts +++ b/src/providers/utils/url.ts @@ -225,7 +225,7 @@ export class CoreUrlUtilsProvider { url = 'https://' + url; } - // http allways in lowercase. + // http always in lowercase. url = url.replace(/^http/i, 'http'); url = url.replace(/^https/i, 'https'); diff --git a/src/singletons/url.ts b/src/singletons/url.ts index 2539d19c6..d777c8f27 100644 --- a/src/singletons/url.ts +++ b/src/singletons/url.ts @@ -119,4 +119,36 @@ export class CoreUrl { return urlParts && urlParts.domain ? urlParts.domain : null; } + /** + * Returns the pattern to check if the URL is a valid Moodle Url. + * + * @return {RegExp} Desired RegExp. + */ + static getValidMoodleUrlPattern(): RegExp { + // Regular expression based on RFC 3986: https://tools.ietf.org/html/rfc3986#appendix-B. + // Improved to not admit spaces. + return new RegExp(/^(([^:/?# ]+):)?(\/\/([^/?# ]*))?([^?# ]*)(\?([^#]*))?(#(.*))?$/); + } + + /** + * Check if the given url is valid for the app to connect. + * + * @param {string} url Url to check. + * @return {boolean} True if valid, false otherwise. + */ + static isValidMoodleUrl(url: string): boolean { + const patt = CoreUrl.getValidMoodleUrlPattern(); + + return patt.test(url.trim()); + } + + /** + * Removes protocol from the url. + * + * @param url Site url. + * @return Url without protocol. + */ + static removeProtocol(url: string): string { + return url.replace(/^[a-zA-Z]+:\/\//i, ''); + } } diff --git a/src/theme/dark.scss b/src/theme/dark.scss index 94ce2b6b1..6411791bc 100644 --- a/src/theme/dark.scss +++ b/src/theme/dark.scss @@ -6,16 +6,6 @@ $core-dark-item-bg-color: $gray-darker !default; $core-dark-item-divider-bg-color: $gray-dark !default; $core-dark-background-color: $black !default; -// Login. -$core-dark-login-page-background-color: radial-gradient(white, $gray-dark) !default; -$core-dark-login-box-background-color: $black !default; -$core-dark-login-box-background-border: $core-login-box-background-border !default; -$core-dark-login-box-text-color: $core-dark-text-color !default; -$core-dark-login-item-inner-background-color: $core-dark-login-box-background-color !default; -$core-dark-login-item-background-color: $core-dark-login-box-background-color !default; -$core-dark-login-button-outline: $core-login-button-outline !default; -$core-dark-login-loading-color: $core-dark-text-color !default; - ion-app.app-root { @include darkmode() { ion-action-sheet .action-sheet-container .action-sheet-group .action-sheet-button { diff --git a/src/theme/variables.scss b/src/theme/variables.scss index aefe54083..61f401678 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -184,16 +184,6 @@ $core-button-outline-background-color: $white !default; $core-network-message-height: 16px !default; -// Login. -$core-login-page-background-color: radial-gradient(white, $gray-light) !default; -$core-login-box-background-color: $white !default; -$core-login-box-background-border: $gray !default; -$core-login-box-text-color: $text-color !default; -$core-login-button-outline: false !default; -$core-login-loading-color: false !default; -$core-login-item-inner-background-color: $white !default; -$core-login-item-background-color: $white !default; - $core-action-sheet-color: $core-color !default; $core-action-sheet-cancel-color: $danger !default; $core-dark-action-sheet-cancel-color: $danger-dark !default;