MOBILE-3565 login: Initial implementation of site page
This commit is contained in:
		
							parent
							
								
									1b227930b7
								
							
						
					
					
						commit
						727db6c4ea
					
				| @ -2112,11 +2112,7 @@ export type CoreSitePublicConfigResponse = { | |||||||
|     mobilecssurl?: string; // Mobile custom CSS theme.
 |     mobilecssurl?: string; // Mobile custom CSS theme.
 | ||||||
|     // eslint-disable-next-line @typescript-eslint/naming-convention
 |     // eslint-disable-next-line @typescript-eslint/naming-convention
 | ||||||
|     tool_mobile_disabledfeatures?: string; // Disabled features in the app.
 |     tool_mobile_disabledfeatures?: string; // Disabled features in the app.
 | ||||||
|     identityproviders?: { // Identity providers.
 |     identityproviders?: CoreSiteIdentityProvider[]; // Identity providers.
 | ||||||
|         name: string; // The identity provider name.
 |  | ||||||
|         iconurl: string; // The icon URL for the provider.
 |  | ||||||
|         url: string; // The URL of the provider.
 |  | ||||||
|     }[]; |  | ||||||
|     country?: string; // Default site country.
 |     country?: string; // Default site country.
 | ||||||
|     agedigitalconsentverification?: boolean; // Whether age digital consent verification is enabled.
 |     agedigitalconsentverification?: boolean; // Whether age digital consent verification is enabled.
 | ||||||
|     supportname?: string; // Site support contact name (only if age verification is enabled).
 |     supportname?: string; // Site support contact name (only if age verification is enabled).
 | ||||||
| @ -2137,6 +2133,15 @@ export type CoreSitePublicConfigResponse = { | |||||||
|     warnings?: CoreWSExternalWarning[]; |     warnings?: CoreWSExternalWarning[]; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Identity provider. | ||||||
|  |  */ | ||||||
|  | export type CoreSiteIdentityProvider = { | ||||||
|  |     name: string; // The identity provider name.
 | ||||||
|  |     iconurl: string; // The icon URL for the provider.
 | ||||||
|  |     url: string; // The URL of the provider.
 | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Result of WS tool_mobile_get_autologin_key. |  * Result of WS tool_mobile_get_autologin_key. | ||||||
|  */ |  */ | ||||||
|  | |||||||
| @ -14,13 +14,17 @@ | |||||||
| 
 | 
 | ||||||
| import { NgModule } from '@angular/core'; | import { NgModule } from '@angular/core'; | ||||||
| import { CommonModule } from '@angular/common'; | import { CommonModule } from '@angular/common'; | ||||||
|  | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; | ||||||
| 
 | 
 | ||||||
| import { IonicModule } from '@ionic/angular'; | import { IonicModule } from '@ionic/angular'; | ||||||
| import { TranslateModule } from '@ngx-translate/core'; | import { TranslateModule } from '@ngx-translate/core'; | ||||||
| 
 | 
 | ||||||
|  | import { CoreComponentsModule } from '@/app/components/components.module'; | ||||||
|  | 
 | ||||||
| import { CoreLoginRoutingModule } from './login-routing.module'; | import { CoreLoginRoutingModule } from './login-routing.module'; | ||||||
| import { CoreLoginInitPage } from './pages/init/init.page'; | import { CoreLoginInitPage } from './pages/init/init.page'; | ||||||
| import { CoreLoginSitePage } from './pages/site/site.page'; | import { CoreLoginSitePage } from './pages/site/site.page'; | ||||||
|  | import { CoreLoginHelperProvider } from './services/helper'; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|     imports: [ |     imports: [ | ||||||
| @ -28,10 +32,16 @@ import { CoreLoginSitePage } from './pages/site/site.page'; | |||||||
|         IonicModule, |         IonicModule, | ||||||
|         CoreLoginRoutingModule, |         CoreLoginRoutingModule, | ||||||
|         TranslateModule.forChild(), |         TranslateModule.forChild(), | ||||||
|  |         FormsModule, | ||||||
|  |         ReactiveFormsModule, | ||||||
|  |         CoreComponentsModule, | ||||||
|     ], |     ], | ||||||
|     declarations: [ |     declarations: [ | ||||||
|         CoreLoginInitPage, |         CoreLoginInitPage, | ||||||
|         CoreLoginSitePage, |         CoreLoginSitePage, | ||||||
|     ], |     ], | ||||||
|  |     providers: [ | ||||||
|  |         CoreLoginHelperProvider, | ||||||
|  |     ], | ||||||
| }) | }) | ||||||
| export class CoreLoginModule {} | export class CoreLoginModule {} | ||||||
|  | |||||||
| @ -13,7 +13,7 @@ | |||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Component, OnInit } from '@angular/core'; | import { Component, OnInit } from '@angular/core'; | ||||||
| import { Router } from '@angular/router'; | import { NavController } from '@ionic/angular'; | ||||||
| 
 | 
 | ||||||
| import { CoreApp } from '@services/app'; | import { CoreApp } from '@services/app'; | ||||||
| import { CoreInit } from '@services/init'; | import { CoreInit } from '@services/init'; | ||||||
| @ -29,7 +29,7 @@ import { SplashScreen } from '@singletons/core.singletons'; | |||||||
| }) | }) | ||||||
| export class CoreLoginInitPage implements OnInit { | export class CoreLoginInitPage implements OnInit { | ||||||
| 
 | 
 | ||||||
|     constructor(protected router: Router) {} |     constructor(protected navCtrl: NavController) {} | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Initialize the component. |      * Initialize the component. | ||||||
| @ -90,6 +90,7 @@ export class CoreLoginInitPage implements OnInit { | |||||||
|         //     return this.loginHelper.goToSiteInitialPage();
 |         //     return this.loginHelper.goToSiteInitialPage();
 | ||||||
|         // }
 |         // }
 | ||||||
| 
 | 
 | ||||||
|         await this.router.navigate(['/login/site']); |         await this.navCtrl.navigateRoot('/login/site'); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,3 +1,105 @@ | |||||||
| <ion-content> | <ion-header> | ||||||
|     {{ 'core.login.yourenteredsite' | translate }} |     <ion-toolbar> | ||||||
|  |         <ion-buttons slot="start"> | ||||||
|  |             <ion-back-button></ion-back-button> | ||||||
|  |         </ion-buttons> | ||||||
|  | 
 | ||||||
|  |         <ion-title>{{ 'core.login.connecttomoodle' | translate }}</ion-title> | ||||||
|  | 
 | ||||||
|  |         <ion-buttons slot="end"> | ||||||
|  |             <!-- @todo: Settings button. --> | ||||||
|  |         </ion-buttons> | ||||||
|  |     </ion-toolbar> | ||||||
|  | </ion-header> | ||||||
|  | <ion-content padding> | ||||||
|  |     <div> | ||||||
|  |         <div text-center padding margin-bottom [class.hidden]="hasSites || enteredSiteUrl" class="core-login-site-logo"> | ||||||
|  |             <img src="assets/img/login_logo.png" class="avatar-full login-logo" role="presentation"> | ||||||
|  |         </div> | ||||||
|  |         <form ion-list [formGroup]="siteForm" (ngSubmit)="connect($event, siteForm.value.siteUrl)" *ngIf="!fixedSites" #siteFormEl> | ||||||
|  |             <!-- Form to input the site URL if there are no fixed sites. --> | ||||||
|  |             <ng-container *ngIf="siteSelector == 'url'"> | ||||||
|  |                 <ion-item> | ||||||
|  |                     <ion-label position="stacked"><h2>{{ 'core.login.siteaddress' | translate }}</h2></ion-label> | ||||||
|  |                     <ion-input name="url" type="url" placeholder="{{ 'core.login.siteaddressplaceholder' | translate }}" formControlName="siteUrl" [core-auto-focus]="showKeyboard && !showScanQR"></ion-input> | ||||||
|  |                 </ion-item> | ||||||
|  |             </ng-container> | ||||||
|  |             <ng-container *ngIf="siteSelector != 'url'"> | ||||||
|  |                 <ion-item> | ||||||
|  |                     <ion-label position="stacked"><h2>{{ 'core.login.siteaddress' | translate }}</h2></ion-label> | ||||||
|  |                     <ion-input name="url" placeholder="{{ 'core.login.siteaddressplaceholder' | translate }}" formControlName="siteUrl" [core-auto-focus]="showKeyboard && !showScanQR" (ionChange)="searchSite($event, siteForm.value.siteUrl)"></ion-input> | ||||||
|  |                 </ion-item> | ||||||
|  | 
 | ||||||
|  |                 <ion-list [class.hidden]="!hasSites && !enteredSiteUrl" class="core-login-site-list"> | ||||||
|  |                     <ion-item no-lines class="core-login-site-list-title"><h2 class="item-heading">{{ 'core.login.selectsite' | translate }}</h2></ion-item> | ||||||
|  |                     <ion-item *ngIf="enteredSiteUrl" (click)="connect($event, enteredSiteUrl.url)" [attr.aria-label]="'core.login.connect' | translate" detail-push class="core-login-entered-site"> | ||||||
|  |                         <ion-thumbnail item-start> | ||||||
|  |                             <core-icon name="fa-pencil"></core-icon> | ||||||
|  |                         </ion-thumbnail> | ||||||
|  |                         <h2 text-wrap>{{ 'core.login.yourenteredsite' | translate }}</h2> | ||||||
|  |                         <p>{{enteredSiteUrl.noProtocolUrl}}</p> | ||||||
|  |                     </ion-item> | ||||||
|  | 
 | ||||||
|  |                     <div class="core-login-site-list-found" [class.hidden]="!hasSites" [class.dimmed]="loadingSites"> | ||||||
|  |                         <div *ngIf="loadingSites" class="core-login-site-list-loading"><ion-spinner></ion-spinner></div> | ||||||
|  |                         <ion-item *ngFor="let site of sites" (click)="connect($event, site.url, site)" [attr.aria-label]="site.name" detail-push> | ||||||
|  |                             <ion-thumbnail item-start *ngIf="siteFinderSettings.displayimage"> | ||||||
|  |                                 <img [src]="site.imageurl" *ngIf="site.imageurl" onError="this.src='assets/icon/icon.png'"> | ||||||
|  |                                 <img src="assets/icon/icon.png" *ngIf="!site.imageurl" class="core-login-default-icon"> | ||||||
|  |                             </ion-thumbnail> | ||||||
|  |                             <h2 *ngIf="site.title" text-wrap>{{site.title}}</h2> | ||||||
|  |                             <p *ngIf="site.noProtocolUrl">{{site.noProtocolUrl}}</p> | ||||||
|  |                             <p *ngIf="site.location">{{site.location}}</p> | ||||||
|  |                         </ion-item> | ||||||
|  |                     </div> | ||||||
|  |                 </ion-list> | ||||||
|  | 
 | ||||||
|  |                 <div *ngIf="!hasSites && loadingSites" class="core-login-site-nolist-loading"><ion-spinner></ion-spinner></div> | ||||||
|  |             </ng-container> | ||||||
|  | 
 | ||||||
|  |             <ion-item *ngIf="siteSelector == 'url'" no-lines> | ||||||
|  |                 <ion-button block [disabled]="!siteForm.valid" text-wrap>{{ 'core.login.connect' | translate }}</ion-button> | ||||||
|  |             </ion-item> | ||||||
|  |         </form> | ||||||
|  | 
 | ||||||
|  |         <ng-container *ngIf="fixedSites"> | ||||||
|  |             <!-- Pick the site from a list of fixed sites. --> | ||||||
|  |             <ion-list *ngIf="siteSelector == 'list'"> | ||||||
|  |                 <ion-item no-lines><h2 class="item-heading">{{ 'core.login.selectsite' | translate }}</h2></ion-item> | ||||||
|  |                 <ion-searchbar *ngIf="fixedSites.length > 4" [(ngModel)]="filter" (ionInput)="filterChanged($event)" (ionCancel)="filterChanged()" [placeholder]="'core.login.findyoursite' | translate"></ion-searchbar> | ||||||
|  |                 <ion-item *ngFor="let site of filteredSites" (click)="connect($event, site.url)" [title]="site.name" detail-push> | ||||||
|  |                     <ion-thumbnail item-start *ngIf="siteFinderSettings.displayimage"> | ||||||
|  |                         <img [src]="site.imageurl" *ngIf="site.imageurl" onError="this.src='assets/icon/icon.png'"> | ||||||
|  |                         <img src="assets/icon/icon.png" *ngIf="!site.imageurl" class="core-login-default-icon"> | ||||||
|  |                     </ion-thumbnail> | ||||||
|  |                     <h2 *ngIf="site.title" text-wrap>{{site.title}}</h2> | ||||||
|  |                     <p *ngIf="site.noProtocolUrl">{{site.noProtocolUrl}}</p> | ||||||
|  |                     <p *ngIf="site.location">{{site.location}}</p> | ||||||
|  |                 </ion-item> | ||||||
|  |             </ion-list> | ||||||
|  | 
 | ||||||
|  |             <!-- Display them using buttons. --> | ||||||
|  |             <div *ngIf="siteSelector == 'buttons'"> | ||||||
|  |                 <p class="padding no-padding-bottom">{{ 'core.login.selectsite' | translate }}</p> | ||||||
|  |                 <ion-button *ngFor="let site of fixedSites" text-wrap block (click)="connect($event, site.url)" [title]="site.name" margin-bottom>{{site.title}}</ion-button> | ||||||
|  |             </div> | ||||||
|  |         </ng-container> | ||||||
|  | 
 | ||||||
|  |         <ng-container *ngIf="showScanQR && !hasSites && !enteredSiteUrl"> | ||||||
|  |             <div class="core-login-site-qrcode-separator">{{ 'core.login.or' | translate }}</div> | ||||||
|  |             <ion-item class="core-login-site-qrcode" no-lines> | ||||||
|  |                 <ion-button block color="light" margin-top icon-start (click)="showInstructionsAndScanQR()" text-wrap> | ||||||
|  |                     <core-icon name="fa-qrcode" aria-hidden="true"></core-icon> | ||||||
|  |                     {{ 'core.scanqr' | translate }} | ||||||
|  |                 </ion-button> | ||||||
|  |             </ion-item> | ||||||
|  |         </ng-container> | ||||||
|  | 
 | ||||||
|  |         <!-- Help. --> | ||||||
|  |         <ion-list no-lines margin-top> | ||||||
|  |             <ion-item text-center text-wrap class="core-login-need-help" (click)="showHelp()" detail-none> | ||||||
|  |                 {{ 'core.needhelp' | translate }} | ||||||
|  |             </ion-item> | ||||||
|  |         </ion-list> | ||||||
|  |     </div> | ||||||
| </ion-content> | </ion-content> | ||||||
|  | |||||||
| @ -12,7 +12,22 @@ | |||||||
| // See the License for the specific language governing permissions and
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Component, OnInit } from '@angular/core'; | import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; | ||||||
|  | import { ActivatedRoute } from '@angular/router'; | ||||||
|  | import { FormBuilder, FormGroup, ValidatorFn, AbstractControl, ValidationErrors } from '@angular/forms'; | ||||||
|  | 
 | ||||||
|  | import { CoreApp } from '@services/app'; | ||||||
|  | import { CoreConfig } from '@services/config'; | ||||||
|  | import { CoreSites, CoreSiteCheckResponse, CoreLoginSiteInfo, CoreSitesDemoSiteData } from '@services/sites'; | ||||||
|  | import { CoreUtils } from '@services/utils/utils'; | ||||||
|  | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
|  | import { CoreLoginHelper, CoreLoginHelperProvider } from '@core/login/services/helper'; | ||||||
|  | import { CoreSite } from '@classes/site'; | ||||||
|  | import { CoreError } from '@classes/errors/error'; | ||||||
|  | import CoreConfigConstants from '@app/config.json'; | ||||||
|  | import { Translate } from '@singletons/core.singletons'; | ||||||
|  | import { CoreUrl } from '@singletons/url'; | ||||||
|  | import { CoreUrlUtils } from '@/app/services/utils/url'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Page that displays a "splash screen" while the app is being initialized. |  * Page that displays a "splash screen" while the app is being initialized. | ||||||
| @ -24,11 +39,477 @@ import { Component, OnInit } from '@angular/core'; | |||||||
| }) | }) | ||||||
| export class CoreLoginSitePage implements OnInit { | export class CoreLoginSitePage implements OnInit { | ||||||
| 
 | 
 | ||||||
|  |     @ViewChild('siteFormEl') formElement: ElementRef; | ||||||
|  | 
 | ||||||
|  |     siteForm: FormGroup; | ||||||
|  |     fixedSites: CoreLoginSiteInfoExtended[]; | ||||||
|  |     filteredSites: CoreLoginSiteInfoExtended[]; | ||||||
|  |     siteSelector = 'sitefinder'; | ||||||
|  |     showKeyboard = false; | ||||||
|  |     filter = ''; | ||||||
|  |     sites: CoreLoginSiteInfoExtended[] = []; | ||||||
|  |     hasSites = false; | ||||||
|  |     loadingSites = false; | ||||||
|  |     searchFunction: (search: string) => void; | ||||||
|  |     showScanQR: boolean; | ||||||
|  |     enteredSiteUrl: CoreLoginSiteInfoExtended; | ||||||
|  |     siteFinderSettings: SiteFinderSettings; | ||||||
|  | 
 | ||||||
|  |     constructor( | ||||||
|  |         protected route: ActivatedRoute, | ||||||
|  |         protected formBuilder: FormBuilder, | ||||||
|  |     ) { | ||||||
|  | 
 | ||||||
|  |         let url = ''; | ||||||
|  |         this.siteSelector = CoreConfigConstants.multisitesdisplay; | ||||||
|  | 
 | ||||||
|  |         const siteFinderSettings: Partial<SiteFinderSettings> = CoreConfigConstants['sitefindersettings'] || {}; | ||||||
|  |         this.siteFinderSettings = { | ||||||
|  |             displaysitename: true, | ||||||
|  |             displayimage: true, | ||||||
|  |             displayalias: true, | ||||||
|  |             displaycity: true, | ||||||
|  |             displaycountry: true, | ||||||
|  |             displayurl: true, | ||||||
|  |             ...siteFinderSettings, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         // Load fixed sites if they're set.
 | ||||||
|  |         if (CoreLoginHelper.instance.hasSeveralFixedSites()) { | ||||||
|  |             url = this.initSiteSelector(); | ||||||
|  |         } else if (CoreConfigConstants.enableonboarding && !CoreApp.instance.isIOS() && !CoreApp.instance.isMac()) { | ||||||
|  |             this.initOnboarding(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.showScanQR = CoreUtils.instance.canScanQR() && (typeof CoreConfigConstants['displayqronsitescreen'] == 'undefined' || | ||||||
|  |             !!CoreConfigConstants['displayqronsitescreen']); | ||||||
|  | 
 | ||||||
|  |         this.siteForm = this.formBuilder.group({ | ||||||
|  |             siteUrl: [url, this.moodleUrlValidator()], | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         this.searchFunction = CoreUtils.instance.debounce(async (search: string) => { | ||||||
|  |             search = search.trim(); | ||||||
|  | 
 | ||||||
|  |             if (search.length >= 3) { | ||||||
|  |                 // Update the sites list.
 | ||||||
|  |                 this.sites = await CoreSites.instance.findSites(search); | ||||||
|  | 
 | ||||||
|  |                 // Add UI tweaks.
 | ||||||
|  |                 this.sites = this.extendCoreLoginSiteInfo(this.sites); | ||||||
|  | 
 | ||||||
|  |                 this.hasSites = !!this.sites.length; | ||||||
|  |             } else { | ||||||
|  |                 // Not reseting the array to allow animation to be displayed.
 | ||||||
|  |                 this.hasSites = false; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             this.loadingSites = false; | ||||||
|  |         }, 1000); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Initialize the component. |      * Initialize the component. | ||||||
|      */ |      */ | ||||||
|     ngOnInit(): void { |     ngOnInit(): void { | ||||||
|         //
 |         this.route.queryParams.subscribe(params => { | ||||||
|  |             this.showKeyboard = !!params['showKeyboard']; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initialize the site selector. | ||||||
|  |      * | ||||||
|  |      * @return URL of the first site. | ||||||
|  |      */ | ||||||
|  |     protected initSiteSelector(): string { | ||||||
|  |         // Deprecate listnourl on 3.9.3, remove this block on the following release.
 | ||||||
|  |         if (this.siteSelector == 'listnourl') { | ||||||
|  |             this.siteSelector = 'list'; | ||||||
|  |             this.siteFinderSettings.displayurl = false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.fixedSites = this.extendCoreLoginSiteInfo(<CoreLoginSiteInfoExtended[]> CoreLoginHelper.instance.getFixedSites()); | ||||||
|  | 
 | ||||||
|  |         // Do not show images if none are set.
 | ||||||
|  |         if (!this.fixedSites.some((site) => !!site.imageurl)) { | ||||||
|  |             this.siteFinderSettings.displayimage = false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Autoselect if not defined.
 | ||||||
|  |         if (this.siteSelector != 'list' && this.siteSelector != 'buttons') { | ||||||
|  |             this.siteSelector = this.fixedSites.length > 3 ? 'list' : 'buttons'; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.filteredSites = this.fixedSites; | ||||||
|  | 
 | ||||||
|  |         return this.fixedSites[0].url; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initialize and show onboarding if needed. | ||||||
|  |      * | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     protected async initOnboarding(): Promise<void> { | ||||||
|  |         const onboardingDone = await CoreConfig.instance.get(CoreLoginHelperProvider.ONBOARDING_DONE, false); | ||||||
|  | 
 | ||||||
|  |         if (!onboardingDone) { | ||||||
|  |             // Check onboarding.
 | ||||||
|  |             this.showOnboarding(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Extend info of Login Site Info to get UI tweaks. | ||||||
|  |      * | ||||||
|  |      * @param  sites Sites list. | ||||||
|  |      * @return Sites list with extended info. | ||||||
|  |      */ | ||||||
|  |     protected extendCoreLoginSiteInfo(sites: CoreLoginSiteInfoExtended[]): CoreLoginSiteInfoExtended[] { | ||||||
|  |         return sites.map((site) => { | ||||||
|  |             site.noProtocolUrl = this.siteFinderSettings.displayurl && site.url ? CoreUrl.removeProtocol(site.url) : ''; | ||||||
|  | 
 | ||||||
|  |             const name = this.siteFinderSettings.displaysitename ? site.name : ''; | ||||||
|  |             const alias = this.siteFinderSettings.displayalias && site.alias ? site.alias : ''; | ||||||
|  | 
 | ||||||
|  |             // Set title with parenthesis if both name and alias are present.
 | ||||||
|  |             site.title = name && alias ? name + ' (' + alias + ')' : name + alias; | ||||||
|  | 
 | ||||||
|  |             const country = this.siteFinderSettings.displaycountry && site.countrycode ? | ||||||
|  |                 CoreUtils.instance.getCountryName(site.countrycode) : ''; | ||||||
|  |             const city = this.siteFinderSettings.displaycity && site.city ? | ||||||
|  |                 site.city : ''; | ||||||
|  | 
 | ||||||
|  |             // Separate location with hiphen if both country and city are present.
 | ||||||
|  |             site.location = city && country ? city + ' - ' + country : city + country; | ||||||
|  | 
 | ||||||
|  |             return site; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Validate Url. | ||||||
|  |      * | ||||||
|  |      * @return {ValidatorFn} Validation results. | ||||||
|  |      */ | ||||||
|  |     protected moodleUrlValidator(): ValidatorFn { | ||||||
|  |         return (control: AbstractControl): ValidationErrors | 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 } }; | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 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): CoreSitesDemoSiteData { | ||||||
|  |         const demoSites = CoreConfigConstants.demo_sites; | ||||||
|  |         if (typeof demoSites != 'undefined' && typeof demoSites[name] != 'undefined') { | ||||||
|  |             return demoSites[name]; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Show a help modal. | ||||||
|  |      */ | ||||||
|  |     showHelp(): void { | ||||||
|  |         // @todo
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Show an onboarding modal. | ||||||
|  |      */ | ||||||
|  |     showOnboarding(): void { | ||||||
|  |         // @todo
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Try to connect to a site. | ||||||
|  |      * | ||||||
|  |      * @param e Event. | ||||||
|  |      * @param url The URL to connect to. | ||||||
|  |      * @param foundSite The site clicked, if any, from the found sites list. | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     async connect(e: Event, url: string, foundSite?: CoreLoginSiteInfoExtended): Promise<void> { | ||||||
|  |         e.preventDefault(); | ||||||
|  |         e.stopPropagation(); | ||||||
|  | 
 | ||||||
|  |         CoreApp.instance.closeKeyboard(); | ||||||
|  | 
 | ||||||
|  |         if (!url) { | ||||||
|  |             CoreDomUtils.instance.showErrorModal('core.login.siteurlrequired', true); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!CoreApp.instance.isOnline()) { | ||||||
|  |             CoreDomUtils.instance.showErrorModal('core.networkerrormsg', true); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         url = url.trim(); | ||||||
|  | 
 | ||||||
|  |         if (url.match(/^(https?:\/\/)?campus\.example\.edu/)) { | ||||||
|  |             this.showLoginIssue(null, new CoreError(Translate.instance.instant('core.login.errorexampleurl'))); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const siteData = CoreSites.instance.getDemoSiteData(url); | ||||||
|  | 
 | ||||||
|  |         if (siteData) { | ||||||
|  |             // It's a demo site.
 | ||||||
|  |             await this.loginDemoSite(siteData); | ||||||
|  | 
 | ||||||
|  |         } else { | ||||||
|  |             // Not a demo site.
 | ||||||
|  |             const modal = await CoreDomUtils.instance.showModalLoading(); | ||||||
|  | 
 | ||||||
|  |             let checkResult: CoreSiteCheckResponse; | ||||||
|  | 
 | ||||||
|  |             try { | ||||||
|  |                 checkResult = await CoreSites.instance.checkSite(url); | ||||||
|  |             } catch (error) { | ||||||
|  |                 // Attempt guessing the domain if the initial check failed
 | ||||||
|  |                 const domain = CoreUrl.guessMoodleDomain(url); | ||||||
|  | 
 | ||||||
|  |                 if (domain && domain != url) { | ||||||
|  |                     try { | ||||||
|  |                         checkResult = await CoreSites.instance.checkSite(domain); | ||||||
|  |                     } catch (secondError) { | ||||||
|  |                         // Try to use the first error.
 | ||||||
|  |                         modal.dismiss(); | ||||||
|  | 
 | ||||||
|  |                         return this.showLoginIssue(url, error || secondError); | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     modal.dismiss(); | ||||||
|  | 
 | ||||||
|  |                     return this.showLoginIssue(url, error); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             await this.login(checkResult, foundSite); | ||||||
|  | 
 | ||||||
|  |             modal.dismiss(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Authenticate in a demo site. | ||||||
|  |      * | ||||||
|  |      * @param siteData Site data. | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     protected async loginDemoSite(siteData: CoreSitesDemoSiteData): Promise<void> { | ||||||
|  |         const modal = await CoreDomUtils.instance.showModalLoading(); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             const data = await CoreSites.instance.getUserToken(siteData.url, siteData.username, siteData.password); | ||||||
|  | 
 | ||||||
|  |             await CoreSites.instance.newSite(data.siteUrl, data.token, data.privateToken); | ||||||
|  | 
 | ||||||
|  |             CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, true); | ||||||
|  | 
 | ||||||
|  |             return CoreLoginHelper.instance.goToSiteInitialPage(); | ||||||
|  |         } catch (error) { | ||||||
|  |             CoreLoginHelper.instance.treatUserTokenError(siteData.url, error, siteData.username, siteData.password); | ||||||
|  | 
 | ||||||
|  |             if (error.loggedout) { | ||||||
|  |                 // @todo Send the user to sites page.
 | ||||||
|  |             } | ||||||
|  |         } finally { | ||||||
|  |             modal.dismiss(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 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, foundSite?: CoreLoginSiteInfoExtended): Promise<void> { | ||||||
|  |         await CoreUtils.instance.ignoreErrors(CoreSites.instance.checkApplication(response)); | ||||||
|  | 
 | ||||||
|  |         CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, true); | ||||||
|  | 
 | ||||||
|  |         if (response.warning) { | ||||||
|  |             CoreDomUtils.instance.showErrorModal(response.warning, true, 4000); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (CoreLoginHelper.instance.isSSOLoginNeeded(response.code)) { | ||||||
|  |             // SSO. User needs to authenticate in a browser.
 | ||||||
|  |             CoreLoginHelper.instance.confirmAndOpenBrowserForSSOLogin( | ||||||
|  |                 response.siteUrl, response.code, response.service, response.config && response.config.launchurl); | ||||||
|  |         } else { | ||||||
|  |             const pageParams = { siteUrl: response.siteUrl, siteConfig: response.config }; | ||||||
|  |             if (foundSite) { | ||||||
|  |                 pageParams['siteName'] = foundSite.name; | ||||||
|  |                 pageParams['logoUrl'] = foundSite.imageurl; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // @todo Navigate to credentials.
 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Show an error that aims people to solve the issue. | ||||||
|  |      * | ||||||
|  |      * @param url The URL the user was trying to connect to. | ||||||
|  |      * @param error Error to display. | ||||||
|  |      */ | ||||||
|  |     protected showLoginIssue(url: string, error: CoreError): void { | ||||||
|  |         let errorMessage = CoreDomUtils.instance.getErrorMessage(error); | ||||||
|  | 
 | ||||||
|  |         if (errorMessage == Translate.instance.instant('core.cannotconnecttrouble')) { | ||||||
|  |             const found = this.sites.find((site) => site.url == url); | ||||||
|  | 
 | ||||||
|  |             if (!found) { | ||||||
|  |                 errorMessage += ' ' + Translate.instance.instant('core.cannotconnectverify'); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let message = '<p>' + errorMessage + '</p>'; | ||||||
|  |         if (url) { | ||||||
|  |             const fullUrl = CoreUrlUtils.instance.isAbsoluteURL(url) ? url : 'https://' + url; | ||||||
|  |             message += '<p padding><a href="' + fullUrl + '" core-link>' + url + '</a></p>'; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const buttons = [ | ||||||
|  |             { | ||||||
|  |                 text: Translate.instance.instant('core.needhelp'), | ||||||
|  |                 handler: (): void => { | ||||||
|  |                     this.showHelp(); | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 text: Translate.instance.instant('core.tryagain'), | ||||||
|  |                 role: 'cancel', | ||||||
|  |             }, | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         // @TODO: Remove CoreSite.MINIMUM_MOODLE_VERSION, not used on translations since 3.9.0.
 | ||||||
|  |         CoreDomUtils.instance.showAlertWithOptions({ | ||||||
|  |             header: Translate.instance.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION }), | ||||||
|  |             message, | ||||||
|  |             buttons, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The filter has changed. | ||||||
|  |      * | ||||||
|  |      * @param event Received Event. | ||||||
|  |      */ | ||||||
|  |     // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||||
|  |     filterChanged(event: any): void { | ||||||
|  |         const newValue = event.target.value?.trim().toLowerCase(); | ||||||
|  |         if (!newValue || !this.fixedSites) { | ||||||
|  |             this.filteredSites = this.fixedSites; | ||||||
|  |         } else { | ||||||
|  |             this.filteredSites = this.fixedSites.filter((site) => | ||||||
|  |                 site.title.toLowerCase().indexOf(newValue) > -1 || site.noProtocolUrl.toLowerCase().indexOf(newValue) > -1 || | ||||||
|  |                 site.location.toLowerCase().indexOf(newValue) > -1); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Find a site on the backend. | ||||||
|  |      * | ||||||
|  |      * @param e Event. | ||||||
|  |      * @param search Text to search. | ||||||
|  |      */ | ||||||
|  |     searchSite(e: Event, search: string): void { | ||||||
|  |         this.loadingSites = true; | ||||||
|  | 
 | ||||||
|  |         search = search.trim(); | ||||||
|  | 
 | ||||||
|  |         if (this.siteForm.valid && search.length >= 3) { | ||||||
|  |             this.enteredSiteUrl = { | ||||||
|  |                 url: search, | ||||||
|  |                 name: 'connect', | ||||||
|  |                 noProtocolUrl: CoreUrl.removeProtocol(search), | ||||||
|  |             }; | ||||||
|  |         } else { | ||||||
|  |             this.enteredSiteUrl = null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.searchFunction(search.trim()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Show instructions and scan QR code. | ||||||
|  |      */ | ||||||
|  |     showInstructionsAndScanQR(): void { | ||||||
|  |         // Show some instructions first.
 | ||||||
|  |         CoreDomUtils.instance.showAlertWithOptions({ | ||||||
|  |             header: Translate.instance.instant('core.login.faqwhereisqrcode'), | ||||||
|  |             message: Translate.instance.instant('core.login.faqwhereisqrcodeanswer', | ||||||
|  |                 { $image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML }), | ||||||
|  |             buttons: [ | ||||||
|  |                 { | ||||||
|  |                     text: Translate.instance.instant('core.cancel'), | ||||||
|  |                     role: 'cancel', | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     text: Translate.instance.instant('core.next'), | ||||||
|  |                     handler: (): void => { | ||||||
|  |                         this.scanQR(); | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Scan a QR code and put its text in the URL input. | ||||||
|  |      * | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     async scanQR(): Promise<void> { | ||||||
|  |         // Scan for a QR code.
 | ||||||
|  |         const text = await CoreUtils.instance.scanQR(); | ||||||
|  | 
 | ||||||
|  |         if (text) { | ||||||
|  |             // @todo
 | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Extended data for UI implementation. | ||||||
|  |  */ | ||||||
|  | type CoreLoginSiteInfoExtended = CoreLoginSiteInfo & { | ||||||
|  |     noProtocolUrl?: string; // Url wihtout protocol.
 | ||||||
|  |     location?: string; // City + country.
 | ||||||
|  |     title?: string; // Name + alias.
 | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | type SiteFinderSettings = { | ||||||
|  |     displayalias: boolean; | ||||||
|  |     displaycity: boolean; | ||||||
|  |     displaycountry: boolean; | ||||||
|  |     displayimage: boolean; | ||||||
|  |     displaysitename: boolean; | ||||||
|  |     displayurl: boolean; | ||||||
|  | }; | ||||||
|  | |||||||
| @ -1,2 +1,130 @@ | |||||||
| app-root page-core-login-init { | .item-input:last-child { | ||||||
|  |     margin-bottom: 20px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .searchbar-ios { | ||||||
|  |     background: transparent; | ||||||
|  | 
 | ||||||
|  |     .searchbar-input { | ||||||
|  |         background-color: white; // @todo $searchbar-ios-toolbar-input-background; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .item.item-block { | ||||||
|  |     &.core-login-need-help.item { | ||||||
|  |         text-decoration: underline; | ||||||
|  |     } | ||||||
|  |     &.core-login-site-qrcode { | ||||||
|  |         .item-inner { | ||||||
|  |             border-bottom: 0; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .core-login-site-connect { | ||||||
|  |     margin-top: 1.4rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .item ion-thumbnail { | ||||||
|  |     min-width: 50px; | ||||||
|  |     min-height: 50px; | ||||||
|  |     width: 50px; | ||||||
|  |     height: 50px; | ||||||
|  |     border-radius: 20%; | ||||||
|  |     box-shadow: 0 0 4px #eee; | ||||||
|  |     text-align: center; | ||||||
|  |     overflow: hidden; | ||||||
|  | 
 | ||||||
|  |     img { | ||||||
|  |         max-height: 50px; | ||||||
|  |         max-width: fit-content; | ||||||
|  |         width: auto; | ||||||
|  |         height: auto; | ||||||
|  |         margin: 0 auto; | ||||||
|  |         margin-left: 50%; | ||||||
|  |         transform: translateX(-50%); | ||||||
|  |         object-fit: cover; | ||||||
|  |         object-position: 50% 50%; | ||||||
|  |     } | ||||||
|  |     ion-icon { | ||||||
|  |         margin: 0 auto; | ||||||
|  |         font-size: 40px; | ||||||
|  |         line-height: 50px; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .core-login-site-logo, | ||||||
|  | .core-login-site-list, | ||||||
|  | .core-login-site-list-found { | ||||||
|  |     transition-delay: 0s; | ||||||
|  |     visibility: visible; | ||||||
|  |     opacity: 1; | ||||||
|  |     transition: all 0.7s ease-in-out; | ||||||
|  |     max-height: 9999px; | ||||||
|  | 
 | ||||||
|  |     &.hidden { | ||||||
|  |         opacity: 0; | ||||||
|  |         visibility: hidden; | ||||||
|  |         margin-top: 0; | ||||||
|  |         margin-bottom: 0; | ||||||
|  |         padding: 0; | ||||||
|  |         max-height: 0; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .core-login-site-list-found.dimmed { | ||||||
|  |     pointer-events: none; | ||||||
|  |     position: relative; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .core-login-site-list-loading { | ||||||
|  |     position: absolute; | ||||||
|  |     //@todo @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; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | /* @todo | ||||||
|  | @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; // @todo $gray-lighter; | ||||||
|  |     ion-thumbnail { | ||||||
|  |         box-shadow: 0 0 4px #ddd; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | .core-login-default-icon { | ||||||
|  |     filter: grayscale(100%); | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										1327
									
								
								src/app/core/login/services/helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1327
									
								
								src/app/core/login/services/helper.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -13,6 +13,7 @@ | |||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | ||||||
|  | import { Params } from '@angular/router'; | ||||||
| import { Subject } from 'rxjs'; | import { Subject } from 'rxjs'; | ||||||
| 
 | 
 | ||||||
| import { CoreLogger } from '@singletons/logger'; | import { CoreLogger } from '@singletons/logger'; | ||||||
| @ -199,3 +200,12 @@ export class CoreEventsProvider { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class CoreEvents extends makeSingleton(CoreEventsProvider) {} | export class CoreEvents extends makeSingleton(CoreEventsProvider) {} | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Data passed to session expired event. | ||||||
|  |  */ | ||||||
|  | export type CoreEventSessionExpiredData = { | ||||||
|  |     pageName?: string; | ||||||
|  |     params?: Params; | ||||||
|  |     siteId?: string; | ||||||
|  | }; | ||||||
|  | |||||||
| @ -276,7 +276,7 @@ export class CoreSitesProvider { | |||||||
|      * @param name Name of the site to check. |      * @param name Name of the site to check. | ||||||
|      * @return Site data if it's a demo site, undefined otherwise. |      * @return Site data if it's a demo site, undefined otherwise. | ||||||
|      */ |      */ | ||||||
|     getDemoSiteData(name: string): {[name: string]: CoreSitesDemoSiteData} { |     getDemoSiteData(name: string): CoreSitesDemoSiteData { | ||||||
|         const demoSites = CoreConfigConstants.demo_sites; |         const demoSites = CoreConfigConstants.demo_sites; | ||||||
|         name = name.toLowerCase(); |         name = name.toLowerCase(); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1304,6 +1304,11 @@ export class CoreDomUtilsProvider { | |||||||
|         needsTranslate?: boolean, |         needsTranslate?: boolean, | ||||||
|         autocloseTime?: number, |         autocloseTime?: number, | ||||||
|     ): Promise<HTMLIonAlertElement | null> { |     ): Promise<HTMLIonAlertElement | null> { | ||||||
|  |         if (this.isCanceledError(error)) { | ||||||
|  |             // It's a canceled error, don't display an error.
 | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         const message = this.getErrorMessage(error, needsTranslate); |         const message = this.getErrorMessage(error, needsTranslate); | ||||||
| 
 | 
 | ||||||
|         if (message === null) { |         if (message === null) { | ||||||
|  | |||||||
							
								
								
									
										
											BIN
										
									
								
								src/assets/img/login_logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/img/login_logo.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 16 KiB | 
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user