MOBILE-3565 login: Initial implementation of site page

main
Dani Palou 2020-10-19 08:55:46 +02:00
parent 1b227930b7
commit 727db6c4ea
11 changed files with 2083 additions and 14 deletions

View File

@ -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.
*/ */

View File

@ -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 {}

View File

@ -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');
} }
} }

View File

@ -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>

View File

@ -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;
};

View File

@ -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

View File

@ -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;
};

View File

@ -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();

View File

@ -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