MOBILE-3565 login: Initial implementation of site page
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%);
|
||||||
}
|
}
|
||||||
|
|
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) {
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
Loading…
Reference in New Issue