forked from EVOgeek/Vmeda.Online
		
	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.
 | ||||
|     // eslint-disable-next-line @typescript-eslint/naming-convention
 | ||||
|     tool_mobile_disabledfeatures?: string; // Disabled features in the app.
 | ||||
|     identityproviders?: { // Identity providers.
 | ||||
|         name: string; // The identity provider name.
 | ||||
|         iconurl: string; // The icon URL for the provider.
 | ||||
|         url: string; // The URL of the provider.
 | ||||
|     }[]; | ||||
|     identityproviders?: CoreSiteIdentityProvider[]; // Identity providers.
 | ||||
|     country?: string; // Default site country.
 | ||||
|     agedigitalconsentverification?: boolean; // Whether age digital consent verification is enabled.
 | ||||
|     supportname?: string; // Site support contact name (only if age verification is enabled).
 | ||||
| @ -2137,6 +2133,15 @@ export type CoreSitePublicConfigResponse = { | ||||
|     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. | ||||
|  */ | ||||
|  | ||||
| @ -14,13 +14,17 @@ | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { CommonModule } from '@angular/common'; | ||||
| import { FormsModule, ReactiveFormsModule } from '@angular/forms'; | ||||
| 
 | ||||
| import { IonicModule } from '@ionic/angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| 
 | ||||
| import { CoreComponentsModule } from '@/app/components/components.module'; | ||||
| 
 | ||||
| import { CoreLoginRoutingModule } from './login-routing.module'; | ||||
| import { CoreLoginInitPage } from './pages/init/init.page'; | ||||
| import { CoreLoginSitePage } from './pages/site/site.page'; | ||||
| import { CoreLoginHelperProvider } from './services/helper'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
| @ -28,10 +32,16 @@ import { CoreLoginSitePage } from './pages/site/site.page'; | ||||
|         IonicModule, | ||||
|         CoreLoginRoutingModule, | ||||
|         TranslateModule.forChild(), | ||||
|         FormsModule, | ||||
|         ReactiveFormsModule, | ||||
|         CoreComponentsModule, | ||||
|     ], | ||||
|     declarations: [ | ||||
|         CoreLoginInitPage, | ||||
|         CoreLoginSitePage, | ||||
|     ], | ||||
|     providers: [ | ||||
|         CoreLoginHelperProvider, | ||||
|     ], | ||||
| }) | ||||
| export class CoreLoginModule {} | ||||
|  | ||||
| @ -13,7 +13,7 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { Router } from '@angular/router'; | ||||
| import { NavController } from '@ionic/angular'; | ||||
| 
 | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreInit } from '@services/init'; | ||||
| @ -29,7 +29,7 @@ import { SplashScreen } from '@singletons/core.singletons'; | ||||
| }) | ||||
| export class CoreLoginInitPage implements OnInit { | ||||
| 
 | ||||
|     constructor(protected router: Router) {} | ||||
|     constructor(protected navCtrl: NavController) {} | ||||
| 
 | ||||
|     /** | ||||
|      * Initialize the component. | ||||
| @ -90,6 +90,7 @@ export class CoreLoginInitPage implements OnInit { | ||||
|         //     return this.loginHelper.goToSiteInitialPage();
 | ||||
|         // }
 | ||||
| 
 | ||||
|         await this.router.navigate(['/login/site']); | ||||
|         await this.navCtrl.navigateRoot('/login/site'); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -1,3 +1,105 @@ | ||||
| <ion-content> | ||||
|     {{ 'core.login.yourenteredsite' | translate }} | ||||
| <ion-header> | ||||
|     <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> | ||||
|  | ||||
| @ -12,7 +12,22 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // 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. | ||||
| @ -24,11 +39,477 @@ import { Component, OnInit } from '@angular/core'; | ||||
| }) | ||||
| 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. | ||||
|      */ | ||||
|     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.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { Params } from '@angular/router'; | ||||
| import { Subject } from 'rxjs'; | ||||
| 
 | ||||
| import { CoreLogger } from '@singletons/logger'; | ||||
| @ -199,3 +200,12 @@ export class 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. | ||||
|      * @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; | ||||
|         name = name.toLowerCase(); | ||||
| 
 | ||||
|  | ||||
| @ -1304,6 +1304,11 @@ export class CoreDomUtilsProvider { | ||||
|         needsTranslate?: boolean, | ||||
|         autocloseTime?: number, | ||||
|     ): 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); | ||||
| 
 | ||||
|         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