MOBILE-3402 login: Improve login page UX
This commit is contained in:
		
							parent
							
								
									38b0f2c391
								
							
						
					
					
						commit
						0d7956c28c
					
				| @ -46,19 +46,6 @@ ion-app.app-root { | ||||
|     text-transform: none; | ||||
|   } | ||||
| 
 | ||||
|   @include media-breakpoint-up(sm) { | ||||
|     .core-center-view .scroll-content { | ||||
|       display: flex!important; | ||||
|       align-content: center !important; | ||||
|       align-items: center !important; | ||||
|       > * { | ||||
|         margin: 0 auto; | ||||
|         width: 100%; | ||||
|         max-width: 600px; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @include media-breakpoint-down(sm) { | ||||
|     .hidden-phone { | ||||
|       display: none !important; | ||||
|  | ||||
| @ -1332,7 +1332,9 @@ | ||||
|     "core.block.blocks": "Blocks", | ||||
|     "core.browser": "Browser", | ||||
|     "core.cancel": "Cancel", | ||||
|     "core.cannotconnect": "<strong>Cannot connect</strong>: Verify that you have correctly typed your site address.", | ||||
|     "core.cannotconnect": "Cannot connect", | ||||
|     "core.cannotconnecttrouble": "We're having trouble connecting to your site.", | ||||
|     "core.cannotconnectverify": "<strong>Please check the address is correct.</strong>", | ||||
|     "core.cannotdownloadfiles": "File downloading is disabled. Please contact your site administrator.", | ||||
|     "core.captureaudio": "Record audio", | ||||
|     "core.capturedimage": "Taken picture.", | ||||
| @ -1758,8 +1760,6 @@ | ||||
|     "core.login.policyagreement": "Site policy agreement", | ||||
|     "core.login.policyagreementclick": "Link to site policy agreement", | ||||
|     "core.login.potentialidps": "Log in using your account on:", | ||||
|     "core.login.problemconnectingerror": "We're having trouble connecting to", | ||||
|     "core.login.problemconnectingerrorcontinue": "Double check you've entered the address correctly and try again.", | ||||
|     "core.login.profileinvaliddata": "Invalid value", | ||||
|     "core.login.recaptchachallengeimage": "reCAPTCHA challenge image", | ||||
|     "core.login.recaptchaexpired": "Verification expired. Answer the security question again.", | ||||
| @ -1773,7 +1773,7 @@ | ||||
|     "core.login.selectacountry": "Select a country", | ||||
|     "core.login.selectsite": "Please select your site:", | ||||
|     "core.login.signupplugindisabled": "{{$a}} is not enabled.", | ||||
|     "core.login.siteaddress": "Your site address", | ||||
|     "core.login.siteaddress": "Your site", | ||||
|     "core.login.sitehasredirect": "Your site contains at least one HTTP redirect. The app cannot follow redirects, this could be the issue that's preventing the app from connecting to your site.", | ||||
|     "core.login.siteinmaintenance": "Your site is in maintenance mode", | ||||
|     "core.login.sitepolicynotagreederror": "Site policy not agreed.", | ||||
| @ -1787,7 +1787,8 @@ | ||||
|     "core.login.usernamerequired": "Username required", | ||||
|     "core.login.usernotaddederror": "User not added - error", | ||||
|     "core.login.visitchangepassword": "Do you want to visit the site to change the password?", | ||||
|     "core.login.webservicesnotenabled": "Web services are not enabled in your site. Please contact your site administrator if you think they should be enabled.", | ||||
|     "core.login.webservicesnotenabled": "Your host site may not have enabled Web services. Please contact your administrator for help.", | ||||
|     "core.login.yourenteredsite": "Connect to your site", | ||||
|     "core.lostconnection": "Your authentication token is invalid or has expired. You will have to reconnect to the site.", | ||||
|     "core.mainmenu.changesite": "Change site", | ||||
|     "core.mainmenu.help": "Help", | ||||
|  | ||||
| @ -73,8 +73,6 @@ | ||||
|     "policyagreement": "Site policy agreement", | ||||
|     "policyagreementclick": "Link to site policy agreement", | ||||
|     "potentialidps": "Log in using your account on:", | ||||
|     "problemconnectingerror": "We're having trouble connecting to", | ||||
|     "problemconnectingerrorcontinue": "Double check you've entered the address correctly and try again.", | ||||
|     "profileinvaliddata": "Invalid value", | ||||
|     "recaptchachallengeimage": "reCAPTCHA challenge image", | ||||
|     "recaptchaexpired": "Verification expired. Answer the security question again.", | ||||
| @ -88,7 +86,7 @@ | ||||
|     "selectacountry": "Select a country", | ||||
|     "selectsite": "Please select your site:", | ||||
|     "signupplugindisabled": "{{$a}} is not enabled.", | ||||
|     "siteaddress": "Your site address", | ||||
|     "siteaddress": "Your site", | ||||
|     "sitehasredirect": "Your site contains at least one HTTP redirect. The app cannot follow redirects, this could be the issue that's preventing the app from connecting to your site.", | ||||
|     "siteinmaintenance": "Your site is in maintenance mode", | ||||
|     "sitepolicynotagreederror": "Site policy not agreed.", | ||||
| @ -102,5 +100,6 @@ | ||||
|     "usernamerequired": "Username required", | ||||
|     "usernotaddederror": "User not added - error", | ||||
|     "visitchangepassword": "Do you want to visit the site to change the password?", | ||||
|     "webservicesnotenabled": "Web services are not enabled in your site. Please contact your site administrator if you think they should be enabled." | ||||
|     "yourenteredsite": "Connect to your site", | ||||
|     "webservicesnotenabled": "Your host site may not have enabled Web services. Please contact your administrator for help." | ||||
| } | ||||
| @ -1,11 +1,28 @@ | ||||
| $core-login-page-background-color: $white !default; | ||||
| $core-login-page-text-color: $text-color !default; | ||||
| $core-login-button-outline: false !default; | ||||
| $core-login-loading-color: false !default; | ||||
| $core-login-item-inner-background-color: $white !default; | ||||
| $core-login-item-background-color: $white !default; | ||||
| 
 | ||||
| // Dark. | ||||
| $core-dark-login-page-background-color: $black !default; | ||||
| $core-dark-login-page-text-color: $core-dark-text-color !default; | ||||
| $core-dark-login-item-inner-background-color: $core-dark-login-page-background-color !default; | ||||
| $core-dark-login-item-background-color: $core-dark-login-page-background-color !default; | ||||
| $core-dark-login-button-outline: $core-login-button-outline !default; | ||||
| $core-dark-login-loading-color: $core-dark-text-color !default; | ||||
| 
 | ||||
| ion-app.app-root page-core-login-credentials, | ||||
| ion-app.app-root page-core-login-reconnect, | ||||
| ion-app.app-root page-core-login-site { | ||||
|     .scroll-content { | ||||
|         background: $core-login-page-background-color; | ||||
|         color: $core-login-page-text-color; | ||||
| 
 | ||||
|         @include darkmode() { | ||||
|             background: $core-dark-login-page-background-color; | ||||
|             color: $core-dark-login-page-text-color; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -13,35 +30,18 @@ ion-app.app-root page-core-login-site { | ||||
|         max-width: 100%; | ||||
|     } | ||||
| 
 | ||||
|     img.login-logo { | ||||
|         width: 90%; | ||||
|         max-width: 300px; | ||||
|     } | ||||
| 
 | ||||
|     .box { | ||||
|         padding: 16px; | ||||
|         margin: 8px; | ||||
|         background: $core-login-box-background-color; | ||||
|         border: 1px solid $core-login-box-background-border; | ||||
|         color: $core-login-box-text-color; | ||||
| 
 | ||||
|         @include darkmode() { | ||||
|             background: $core-dark-login-box-background-color; | ||||
|             border-color: $core-dark-login-box-background-border; | ||||
|             color: $core-dark-login-box-text-color; | ||||
|         } | ||||
| 
 | ||||
|         .item { | ||||
|             @include darkmode() { | ||||
|                 background: $core-dark-login-box-background-color; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .core-sitename, .core-siteurl { | ||||
|         @if $core-fixed-url { display: none; } | ||||
|     } | ||||
| 
 | ||||
|     .core-sitename + .core-siteurl  { | ||||
|         margin-top: 0; | ||||
|     } | ||||
| 
 | ||||
|     .core-sitename { | ||||
|         font-size: 1.8rem; | ||||
|     } | ||||
| 
 | ||||
|     @if $core-login-button-outline { | ||||
|         .button-md.button-default-md, .button-ios.button-default-ios { | ||||
|             @extend .button-md-light; | ||||
| @ -77,4 +77,27 @@ ion-app.app-root page-core-login-site { | ||||
|     .item-input { | ||||
|         margin-bottom: 20px; | ||||
|     } | ||||
| 
 | ||||
|     ion-list.core-login-forgotten-password { | ||||
|         margin-top: 0; | ||||
|         margin-bottom: 0; | ||||
| 
 | ||||
|         a.item { | ||||
|             background: transparent; | ||||
|             text-decoration: underline; | ||||
| 
 | ||||
|             @include darkmode() { | ||||
|                 background: transparent; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .core-login-site-logo { | ||||
|         margin-top: 5px; | ||||
|         margin-bottom: 5px; | ||||
|         img { | ||||
|             width: 90%; | ||||
|             max-width: 300px; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -8,7 +8,7 @@ | ||||
|         </ion-buttons> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <ion-content padding class="core-center-view"> | ||||
| <ion-content padding> | ||||
|     <ion-list> | ||||
|         <ion-item text-wrap *ngIf="!changingPassword"> | ||||
|             <h2>{{ 'core.login.forcepasswordchangenotice' | translate }}</h2> | ||||
|  | ||||
| @ -9,52 +9,54 @@ | ||||
|         </ion-buttons> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <ion-content class="core-center-view"> | ||||
| <ion-content padding> | ||||
|     <core-loading [hideUntil]="pageLoaded"> | ||||
|         <div class="box"> | ||||
|             <div text-wrap text-center margin-bottom> | ||||
|         <div text-wrap text-center margin-bottom> | ||||
|             <div class="core-login-site-logo"> | ||||
|                 <!-- Show site logo or a default image. --> | ||||
|                 <img *ngIf="logoUrl" [src]="logoUrl" role="presentation"> | ||||
|                 <img *ngIf="!logoUrl" src="assets/img/login_logo.png" class="login-logo" role="presentation"> | ||||
| 
 | ||||
|                 <!-- If no sitename show big siteurl. --> | ||||
|                 <p *ngIf="!siteName" padding class="item-heading core-siteurl">{{siteUrl}}</p> | ||||
|                 <!-- If sitename, show big sitename and small siteurl. --> | ||||
|                 <p *ngIf="siteName" padding class="item-heading core-sitename"><core-format-text [text]="siteName" [filter]="false"></core-format-text></p> | ||||
|                 <p *ngIf="siteName" class="core-siteurl">{{siteUrl}}</p> | ||||
|             </div> | ||||
|             <form ion-list [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #credentialsForm> | ||||
|                 <ion-item *ngIf="siteChecked && !isBrowserSSO"> | ||||
|                     <ion-input type="text" name="username" placeholder="{{ 'core.login.username' | translate }}" formControlName="username" autocapitalize="none" autocorrect="off"></ion-input> | ||||
|                 </ion-item> | ||||
|                 <ion-item *ngIf="siteChecked && !isBrowserSSO" margin-bottom> | ||||
|                     <core-show-password item-content [name]="'password'"> | ||||
|                         <ion-input class="core-ioninput-password" name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" formControlName="password" core-show-password [clearOnEdit]="false"></ion-input> | ||||
|                     </core-show-password> | ||||
|                 </ion-item> | ||||
|                 <button ion-button block [disabled]="siteChecked && !isBrowserSSO && !credForm.valid">{{ 'core.login.loginbutton' | translate }}</button> | ||||
|             </form> | ||||
| 
 | ||||
|             <!-- Forgotten password button. --> | ||||
|             <div *ngIf="showForgottenPassword" padding-top class="core-login-forgotten-password"> | ||||
|                 <button ion-button block text-wrap color="light" (click)="forgottenPassword()">{{ 'core.login.forgotten' | translate }}</button> | ||||
|                 <img *ngIf="!logoUrl" src="assets/img/login_logo.png" role="presentation"> | ||||
|             </div> | ||||
| 
 | ||||
|             <ion-list *ngIf="identityProviders && identityProviders.length" padding-top class="core-login-identity-providers"> | ||||
|                 <ion-list-header text-wrap>{{ 'core.login.potentialidps' | translate }}</ion-list-header> | ||||
|                 <button ion-item *ngFor="let provider of identityProviders" text-wrap class="core-oauth-icon" (click)="oauthClicked(provider)" title="{{provider.name}}"> | ||||
|                     <img [src]="provider.iconurl" alt="" width="32" height="32" item-start> | ||||
|                     {{provider.name}} | ||||
|                 </button> | ||||
|             </ion-list> | ||||
| 
 | ||||
|             <ion-list *ngIf="canSignup" padding-top class="core-login-sign-up"> | ||||
|                 <ion-list-header text-wrap>{{ 'core.login.firsttime' | translate }}</ion-list-header> | ||||
|                 <ion-item no-lines text-wrap *ngIf="authInstructions"> | ||||
|                     <p><core-format-text [text]="authInstructions" [filter]="false"></core-format-text></p> | ||||
|                 </ion-item> | ||||
|                 <button ion-button block (click)="signup()">{{ 'core.login.startsignup' | translate }}</button> | ||||
|             </ion-list> | ||||
|             <h3 *ngIf="siteName" padding class="core-sitename"><core-format-text [text]="siteName" [filter]="false"></core-format-text></h3> | ||||
|             <p class="core-siteurl">{{siteUrl}}</p> | ||||
|         </div> | ||||
| 
 | ||||
|         <form ion-list [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #credentialsForm> | ||||
|             <ion-item *ngIf="siteChecked && !isBrowserSSO"> | ||||
|                 <ion-input type="text" name="username" placeholder="{{ 'core.login.username' | translate }}" formControlName="username" autocapitalize="none" autocorrect="off"></ion-input> | ||||
|             </ion-item> | ||||
|             <ion-item *ngIf="siteChecked && !isBrowserSSO" margin-bottom> | ||||
|                 <core-show-password item-content [name]="'password'"> | ||||
|                     <ion-input class="core-ioninput-password" name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" formControlName="password" core-show-password [clearOnEdit]="false"></ion-input> | ||||
|                 </core-show-password> | ||||
|             </ion-item> | ||||
|             <div padding> | ||||
|                 <button ion-button block [disabled]="siteChecked && !isBrowserSSO && !credForm.valid" class="core-login-login-button">{{ 'core.login.loginbutton' | translate }}</button> | ||||
|             </div> | ||||
|         </form> | ||||
| 
 | ||||
|         <!-- Forgotten password button. --> | ||||
|         <ion-list no-lines *ngIf="showForgottenPassword" class="core-login-forgotten-password"> | ||||
|             <a ion-item text-center text-wrap (click)="forgottenPassword()" detail-none> | ||||
|                 {{ 'core.login.forgotten' | translate }} | ||||
|             </a> | ||||
|         </ion-list> | ||||
| 
 | ||||
|         <ion-list *ngIf="identityProviders && identityProviders.length" padding-top class="core-login-identity-providers"> | ||||
|             <ion-item text-wrap no-lines><h3 class="item-heading">{{ 'core.login.potentialidps' | translate }}</h3></ion-item> | ||||
|             <button ion-item *ngFor="let provider of identityProviders" text-wrap class="core-oauth-icon" (click)="oauthClicked(provider)" title="{{provider.name}}"> | ||||
|                 <img [src]="provider.iconurl" alt="" width="32" height="32" item-start> | ||||
|                 {{provider.name}} | ||||
|             </button> | ||||
|         </ion-list> | ||||
| 
 | ||||
|         <ion-list *ngIf="canSignup" padding-top class="core-login-sign-up"> | ||||
|             <ion-item text-wrap no-lines><h3 class="item-heading">{{ 'core.login.firsttime' | translate }}</h3></ion-item> | ||||
|             <ion-item no-lines text-wrap *ngIf="authInstructions"> | ||||
|                 <p><core-format-text [text]="authInstructions" [filter]="false"></core-format-text></p> | ||||
|             </ion-item> | ||||
|             <button ion-button block color="light" (click)="signup()">{{ 'core.login.startsignup' | translate }}</button> | ||||
|         </ion-list> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
|  | ||||
| @ -1,5 +0,0 @@ | ||||
| ion-app.app-root page-core-login-credentials { | ||||
|     .item-input { | ||||
|         margin-bottom: 20px; | ||||
|     } | ||||
| } | ||||
| @ -65,6 +65,8 @@ export class CoreLoginCredentialsPage { | ||||
|             private eventsProvider: CoreEventsProvider) { | ||||
| 
 | ||||
|         this.siteUrl = navParams.get('siteUrl'); | ||||
|         this.siteName = navParams.get('siteName') || null; | ||||
|         this.logoUrl = navParams.get('logoUrl') || null; | ||||
|         this.siteConfig = navParams.get('siteConfig'); | ||||
|         this.urlToOpen = navParams.get('urlToOpen'); | ||||
| 
 | ||||
| @ -170,8 +172,6 @@ export class CoreLoginCredentialsPage { | ||||
|                 this.eventsProvider.trigger(CoreEventsProvider.LOGIN_SITE_CHECKED, { config: this.siteConfig }); | ||||
|             } | ||||
|         } else { | ||||
|             this.siteName = null; | ||||
|             this.logoUrl = null; | ||||
|             this.authInstructions = null; | ||||
|             this.canSignup = false; | ||||
|             this.identityProviders = []; | ||||
|  | ||||
| @ -3,72 +3,69 @@ | ||||
|         <ion-title>{{ 'core.login.reconnect' | translate }}</ion-title> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <ion-content class="core-center-view"> | ||||
|     <div class="box"> | ||||
|         <div *ngIf="site" text-wrap text-center margin-bottom [ngClass]="{'item-avatar-center': showSiteAvatar}"> | ||||
|             <ng-container *ngIf="showSiteAvatar"> | ||||
|                 <ion-avatar> | ||||
|                     <!-- Show user avatar. --> | ||||
|                     <img [src]="site.avatar" class="avatar" core-external-content [siteId]="site.id" alt="{{ 'core.pictureof' | translate:{$a: site.fullname} }}" role="presentation" onError="this.src='assets/img/user-avatar.png'"> | ||||
|                 </ion-avatar> | ||||
|             </ng-container> | ||||
| <ion-content padding> | ||||
|     <div *ngIf="site" text-wrap text-center margin-bottom [ngClass]="{'item-avatar-center': showSiteAvatar}"> | ||||
|         <ng-container *ngIf="showSiteAvatar"> | ||||
|             <ion-avatar> | ||||
|                 <!-- Show user avatar. --> | ||||
|                 <img [src]="site.avatar" class="avatar" core-external-content [siteId]="site.id" alt="{{ 'core.pictureof' | translate:{$a: site.fullname} }}" role="presentation" onError="this.src='assets/img/user-avatar.png'"> | ||||
|             </ion-avatar> | ||||
|         </ng-container> | ||||
| 
 | ||||
|             <ng-container *ngIf="!showSiteAvatar"> | ||||
|                 <!-- Show site logo or a default image. --> | ||||
|                 <img *ngIf="logoUrl" [src]="logoUrl" core-external-content [siteId]="site.id" role="presentation"> | ||||
|                 <img *ngIf="!logoUrl" src="assets/img/login_logo.png" class="login-logo" role="presentation"> | ||||
|             </ng-container> | ||||
| 
 | ||||
|             <!-- If no sitename show big siteurl. --> | ||||
|             <p *ngIf="!siteName" class="item-heading core-siteurl">{{siteUrl}}</p> | ||||
|             <!-- If sitename, show big sitename and small siteurl. --> | ||||
|             <p *ngIf="siteName" class="item-heading core-sitename"><core-format-text [text]="siteName" [filter]="false"></core-format-text></p> | ||||
|             <p *ngIf="siteName" class="core-siteurl">{{siteUrl}}</p> | ||||
| 
 | ||||
|             <p *ngIf="!isLoggedOut"> | ||||
|                 <ion-icon padding name="alert"></ion-icon> {{ 'core.login.reconnectdescription' | translate }} | ||||
|             </p> | ||||
|         </div> | ||||
|         <form ion-list *ngIf="!isOAuth" [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #reconnectForm> | ||||
|             <ion-item text-wrap class="core-username"> | ||||
|                 <p>{{username}}</p> | ||||
|             </ion-item> | ||||
|             <ion-item margin-bottom> | ||||
|                 <core-show-password item-content [name]="'password'"> | ||||
|                     <ion-input class="core-ioninput-password" name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [clearOnEdit]="false"></ion-input> | ||||
|                 </core-show-password> | ||||
|             </ion-item> | ||||
|             <ion-grid no-padding> | ||||
|                 <ion-row> | ||||
|                     <ion-col> | ||||
|                         <a ion-button block color="light" (click)="cancel($event)">{{ 'core.login.cancel' | translate }}</a> | ||||
|                     </ion-col> | ||||
|                     <ion-col> | ||||
|                         <button ion-button block [disabled]="!credForm.valid">{{ 'core.login.loginbutton' | translate }}</button> | ||||
|                     </ion-col> | ||||
|                 </ion-row> | ||||
|             </ion-grid> | ||||
|         </form> | ||||
| 
 | ||||
|         <!-- Forgotten password button. --> | ||||
|         <div *ngIf="showForgottenPassword && !isOAuth" padding-top class="core-login-forgotten-password"> | ||||
|             <button ion-button block text-wrap color="light" (click)="forgottenPassword()">{{ 'core.login.forgotten' | translate }}</button> | ||||
|         <div class="core-login-site-logo" *ngIf="!showSiteAvatar"> | ||||
|             <!-- Show site logo or a default image. --> | ||||
|             <img *ngIf="logoUrl" [src]="logoUrl" core-external-content [siteId]="siteId" role="presentation"> | ||||
|             <img *ngIf="!logoUrl" src="assets/img/login_logo.png" role="presentation"> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Identity providers. --> | ||||
|         <ion-list *ngIf="identityProviders && identityProviders.length" padding-top class="core-login-identity-providers"> | ||||
|             <ion-list-header text-wrap>{{ 'core.login.potentialidps' | translate }}</ion-list-header> | ||||
|             <button ion-item *ngFor="let provider of identityProviders" text-wrap class="core-oauth-icon" (click)="oauthClicked(provider)" title="{{provider.name}}"> | ||||
|                 <img [src]="provider.iconurl" alt="" width="32" height="32" item-start> | ||||
|                 {{provider.name}} | ||||
|             </button> | ||||
|         </ion-list> | ||||
|         <h3 *ngIf="siteName" padding class="core-sitename"><core-format-text [text]="siteName" [filter]="false"></core-format-text></h3> | ||||
|         <p class="core-siteurl">{{siteUrl}}</p> | ||||
| 
 | ||||
|         <!-- If OAuth, display cancel button since the form isn't displayed. --> | ||||
|         <ion-list *ngIf="isOAuth"> | ||||
|             <ion-item> | ||||
|                 <a ion-button block color="light" (click)="cancel($event)">{{ 'core.login.cancel' | translate }}</a> | ||||
|             </ion-item> | ||||
|         </ion-list> | ||||
|         <p *ngIf="!isLoggedOut" class="core-login-reconnect-warning"> | ||||
|             <ion-icon padding name="alert"></ion-icon> {{ 'core.login.reconnectdescription' | translate }} | ||||
|         </p> | ||||
|     </div> | ||||
|     <form ion-list *ngIf="!isOAuth" [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #reconnectForm> | ||||
|         <ion-item text-wrap class="core-username"> | ||||
|             <p>{{username}}</p> | ||||
|         </ion-item> | ||||
|         <ion-item margin-bottom> | ||||
|             <core-show-password item-content [name]="'password'"> | ||||
|                 <ion-input class="core-ioninput-password" name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [clearOnEdit]="false"></ion-input> | ||||
|             </core-show-password> | ||||
|         </ion-item> | ||||
|         <ion-grid padding> | ||||
|             <ion-row> | ||||
|                 <ion-col> | ||||
|                     <a ion-button block color="light" (click)="cancel($event)">{{ 'core.login.cancel' | translate }}</a> | ||||
|                 </ion-col> | ||||
|                 <ion-col> | ||||
|                     <button ion-button block [disabled]="!credForm.valid">{{ 'core.login.loginbutton' | translate }}</button> | ||||
|                 </ion-col> | ||||
|             </ion-row> | ||||
|         </ion-grid> | ||||
|     </form> | ||||
| 
 | ||||
|     <!-- Forgotten password button. --> | ||||
|     <ion-list no-lines *ngIf="showForgottenPassword && !isOAuth" class="core-login-forgotten-password"> | ||||
|         <a ion-item text-center text-wrap (click)="forgottenPassword()" detail-none> | ||||
|             {{ 'core.login.forgotten' | translate }} | ||||
|         </a> | ||||
|     </ion-list> | ||||
| 
 | ||||
|     <!-- Identity providers. --> | ||||
|     <ion-list *ngIf="identityProviders && identityProviders.length" padding-top class="core-login-identity-providers"> | ||||
|         <ion-item text-wrap><h3 class="item-heading">{{ 'core.login.potentialidps' | translate }}</h3></ion-item> | ||||
|         <button ion-item *ngFor="let provider of identityProviders" text-wrap class="core-oauth-icon" (click)="oauthClicked(provider)" title="{{provider.name}}"> | ||||
|             <img [src]="provider.iconurl" alt="" width="32" height="32" item-start> | ||||
|             {{provider.name}} | ||||
|         </button> | ||||
|     </ion-list> | ||||
| 
 | ||||
|     <!-- If OAuth, display cancel button since the form isn't displayed. --> | ||||
|     <ion-list *ngIf="isOAuth"> | ||||
|         <ion-item> | ||||
|             <a ion-button block color="light" (click)="cancel($event)">{{ 'core.login.cancel' | translate }}</a> | ||||
|         </ion-item> | ||||
|     </ion-list> | ||||
| </ion-content> | ||||
|  | ||||
| @ -30,7 +30,7 @@ ion-app.app-root page-core-login-reconnect { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .item-input { | ||||
|         margin-bottom: 20px; | ||||
|     .core-login-reconnect-warning { | ||||
|         color: $red; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,27 +0,0 @@ | ||||
| <ion-header> | ||||
|     <ion-navbar core-back-button> | ||||
|         <ion-title>{{ 'core.error' | translate }}</ion-title> | ||||
| 
 | ||||
|         <ion-buttons end> | ||||
|             <button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate"> | ||||
|                 <ion-icon name="close"></ion-icon> | ||||
|             </button> | ||||
|         </ion-buttons> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <ion-content padding> | ||||
|     <h3>{{ 'core.whoops' | translate }}</h3> | ||||
|     <p>{{ 'core.login.problemconnectingerror' | translate }}</p> | ||||
|     <p padding>{{siteUrl}}</p> | ||||
|     <p>{{ 'core.login.problemconnectingerrorcontinue' | translate }}</p> | ||||
|     <button ion-button block (click)="closeModal()">{{ 'core.tryagain' | translate }}</button> | ||||
|     <h3>{{ 'core.login.stillcantconnect' | translate }}</h3> | ||||
|     <p>{{ 'core.login.contactyouradministrator' | translate }}</p> | ||||
|     <p *ngIf="issue"> | ||||
|         {{ 'core.login.contactyouradministratorissue' | translate:{$a: ''} }} | ||||
|     </p> | ||||
|     <p *ngIf="issue" margin-bottom> | ||||
|         <core-format-text [text]="issue" [filter]="false"></core-format-text> | ||||
|     </p> | ||||
| </ion-content> | ||||
| 
 | ||||
| @ -1,31 +0,0 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { IonicPageModule } from 'ionic-angular'; | ||||
| import { CoreLoginSiteErrorPage } from './site-error'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { CoreDirectivesModule } from '@directives/directives.module'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         CoreLoginSiteErrorPage | ||||
|     ], | ||||
|     imports: [ | ||||
|         CoreDirectivesModule, | ||||
|         IonicPageModule.forChild(CoreLoginSiteErrorPage), | ||||
|         TranslateModule.forChild() | ||||
|     ] | ||||
| }) | ||||
| export class CoreLoginSiteErrorPageModule {} | ||||
| @ -1,3 +0,0 @@ | ||||
|     page-core-login-site-error button.button.button-block { | ||||
|         margin-bottom: 3rem; | ||||
|     } | ||||
| @ -1,41 +0,0 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component } from '@angular/core'; | ||||
| import { IonicPage, ViewController, NavParams } from 'ionic-angular'; | ||||
| 
 | ||||
| /** | ||||
|  * Component that displays an error when trying to connect to a site. | ||||
|  */ | ||||
| @IonicPage({ segment: 'core-login-site-error' }) | ||||
| @Component({ | ||||
|     selector: 'page-core-login-site-error', | ||||
|     templateUrl: 'site-error.html', | ||||
| }) | ||||
| export class CoreLoginSiteErrorPage { | ||||
|     siteUrl: string; | ||||
|     issue: string; | ||||
| 
 | ||||
|     constructor(private viewCtrl: ViewController, params: NavParams) { | ||||
|         this.siteUrl = params.get('siteUrl'); | ||||
|         this.issue = params.get('issue'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Close modal. | ||||
|      */ | ||||
|     closeModal(): void { | ||||
|         this.viewCtrl.dismiss(); | ||||
|     } | ||||
| } | ||||
| @ -9,9 +9,9 @@ | ||||
|         </ion-buttons> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| <ion-content class="core-center-view"> | ||||
|     <div class="box"> | ||||
|         <div text-center padding> | ||||
| <ion-content padding> | ||||
|     <div> | ||||
|         <div text-center padding margin-bottom [class.hidden]="hasSites" 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 || fixedDisplay == 'select'" #siteFormEl> | ||||
| @ -19,19 +19,36 @@ | ||||
|             <ng-container *ngIf="!fixedSites"> | ||||
|                 <ion-item> | ||||
|                     <ion-label stacked><h2>{{ 'core.login.siteaddress' | translate }}</h2></ion-label> | ||||
|                     <ion-input type="url" name="url" placeholder="https://campus.example.edu" formControlName="siteUrl" [core-auto-focus]="showKeyboard"></ion-input> | ||||
|                     <ion-input type="url" name="url" placeholder="https://campus.example.edu" formControlName="siteUrl" [core-auto-focus]="showKeyboard" (ionChange)="searchSite($event, siteForm.value.siteUrl)"></ion-input> | ||||
|                 </ion-item> | ||||
|             </ng-container> | ||||
| 
 | ||||
|             <ion-list *ngIf="!fixedSites" [class.hidden]="!hasSites" class="core-login-site-list" [class.dimmed]="loadingSites"> | ||||
|                 <div *ngIf="loadingSites" class="core-login-site-list-loading"><ion-spinner></ion-spinner></div> | ||||
|                 <ion-item no-lines class="core-login-site-list-title" *ngIf="onlyWrittenSite"><h2 class="item-heading">{{ 'core.login.selectsite' | translate }}</h2></ion-item> | ||||
|                 <ion-item *ngFor="let site of sites" (click)="connect($event, site.url, site)" [title]="site.name" detail-push [class.core-login-entered-site]="!site.fromWS"> | ||||
|                     <ion-thumbnail item-start> | ||||
|                         <core-icon name="fa-pencil" *ngIf="!site.imageurl && !site.fromWS"></core-icon> | ||||
|                         <img [src]="site.imageurl" *ngIf="site.imageurl"> | ||||
|                         <img src="assets/icon/icon.png" *ngIf="!site.imageurl && site.fromWS" class="core-login-default-icon"> | ||||
|                     </ion-thumbnail> | ||||
|                     <h2 text-wrap>{{site.name}}<ng-container *ngIf="site.alias"> ({{site.alias}})</ng-container></h2> | ||||
|                     <p>{{site.noProtocolUrl}}</p> | ||||
|                     <p *ngIf="site.country || site.city" text-wrap><ng-container *ngIf="site.city">{{site.city}} - </ng-container>{{site.country}}</p> | ||||
|                 </ion-item> | ||||
|             </ion-list> | ||||
| 
 | ||||
|             <div *ngIf="!fixedSites && !hasSites && loadingSites" class="core-login-site-nolist-loading"><ion-spinner></ion-spinner></div> | ||||
| 
 | ||||
|             <!-- Pick the site from a list of fixed sites. --> | ||||
|             <ion-item *ngIf="fixedSites && fixedDisplay == 'select'" margin-vertical text-wrap> | ||||
|                 <ion-label stacked for="siteSelect">{{ 'core.login.selectsite' | translate }}</ion-label> | ||||
|                 <ion-select formControlName="siteUrl" name="url" placeholder="{{ 'core.login.siteaddress' | translate }}" interface="action-sheet"> | ||||
|                     <ion-option *ngFor="let site of fixedSites" [value]="site.url">{{site.name}}</ion-option> | ||||
|                 </ion-select> | ||||
|             </ion-item> | ||||
| 
 | ||||
|             <button ion-button block [disabled]="!siteForm.valid">{{ 'core.login.connect' | translate }}</button> | ||||
|         </form> | ||||
| 
 | ||||
|         <!-- Pick the site from a list of fixed sites. --> | ||||
|         <ion-list *ngIf="fixedSites && (fixedDisplay == 'list' || fixedDisplay == 'listnourl')"> | ||||
|             <ion-item no-lines><h2 class="item-heading">{{ 'core.login.selectsite' | translate }}</h2></ion-item> | ||||
| @ -48,33 +65,9 @@ | ||||
|             <a *ngFor="let site of fixedSites" ion-button block (click)="connect($event, site.url)" [title]="site.name" margin-bottom>{{site.name}}</a> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Error. --> | ||||
|         <div padding-top *ngIf="error" > | ||||
|             <ion-card class="core-site-error"> | ||||
|                 <ion-card-header> | ||||
|                     {{ 'core.whoops' | translate }} | ||||
|                 </ion-card-header> | ||||
|                 <ion-card-content> | ||||
|                     <p><core-format-text [text]="error.message" [filter]="false"></core-format-text></p> | ||||
|                     <ng-container *ngIf="error.url"> | ||||
|                         <p>{{ 'core.login.problemconnectingerror' | translate }}</p> | ||||
|                         <p padding><a [href]="error.fullUrl" core-link>{{ error.url }}</a></p> | ||||
|                         <p><strong>{{ 'core.login.problemconnectingerrorcontinue' | translate }}</strong></p> | ||||
|                     </ng-container> | ||||
|                 </ion-card-content> | ||||
|                 <ion-card-header> | ||||
|                     {{ 'core.login.stillcantconnect' | translate }} | ||||
|                 </ion-card-header> | ||||
|                 <ion-card-content> | ||||
|                     <p>{{ 'core.login.contactyouradministrator' | translate }}</p> | ||||
|                     <p>{{ 'core.whoissiteadmin' | translate }}</p> | ||||
|                 </ion-card-content> | ||||
|             </ion-card> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Help. --> | ||||
|         <ion-list no-lines> | ||||
|             <a ion-item text-center class="core-login-need-help" (click)="showHelp()" detail-none> | ||||
|         <ion-list no-lines margin-top> | ||||
|             <a ion-item text-center text-wrap class="core-login-need-help" (click)="showHelp()" detail-none> | ||||
|                 {{ 'core.needhelp' | translate }} | ||||
|             </a> | ||||
|         </ion-list> | ||||
|  | ||||
| @ -11,23 +11,108 @@ ion-app.app-root page-core-login-site { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .core-site-error { | ||||
|         background: $red-light; | ||||
|         margin-left: 0; | ||||
|         margin-right: 0; | ||||
|         width: 100%; | ||||
|         user-select: text; | ||||
| 
 | ||||
|         p, ion-card-header { | ||||
|             color: $red-dark; | ||||
|             user-select: text; | ||||
|         } | ||||
|         ion-card-header { | ||||
|             font-weight: bold; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .core-login-need-help { | ||||
|     .core-login-need-help.item { | ||||
|         background: transparent; | ||||
|         text-decoration: underline; | ||||
| 
 | ||||
|         @include darkmode() { | ||||
|             background: transparent; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|     .core-login-site-connect { | ||||
|         margin-top: 1.4rem; | ||||
|     } | ||||
| 
 | ||||
|     .item ion-thumbnail { | ||||
|         min-width: 50px; | ||||
|         min-height: 50px; | ||||
|         border-radius: 20%; | ||||
|         box-shadow: 0 0 4px #eee; | ||||
|         text-align: center; | ||||
| 
 | ||||
|         img { | ||||
|             width: 50px; | ||||
|             height: 50px; | ||||
|         } | ||||
|         ion-icon { | ||||
|             margin: 0 auto; | ||||
|             font-size: 40px; | ||||
|             line-height: 50px; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .core-login-site-logo, | ||||
|     .core-login-site-list { | ||||
|         transition-delay: 0s; | ||||
|         visibility: visible; | ||||
|         opacity: 1; | ||||
|         transition: all 0.7s ease-in-out; | ||||
|         max-height: 9999px; | ||||
| 
 | ||||
|         &.hidden { | ||||
|             opacity: 0; | ||||
|             visibility: hidden; | ||||
|             margin: 0; | ||||
|             padding: 0; | ||||
|             max-height: 0; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .core-login-site-list.dimmed { | ||||
|         pointer-events: none; | ||||
|         position: relative; | ||||
|     } | ||||
| 
 | ||||
|     .core-login-site-list-loading { | ||||
|         position: absolute; | ||||
|         @include position(0, 0, 0, 0); | ||||
|         width: 100%; | ||||
|         height: 100%; | ||||
|         display: flex; | ||||
|         align-content: center; | ||||
|         align-items: center; | ||||
|         background-color: rgba(255, 255, 255, 0.5); | ||||
|         z-index: 1; | ||||
|         ion-spinner { | ||||
|             flex: 1; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .core-login-site-nolist-loading { | ||||
|         text-align: center; | ||||
|     } | ||||
| 
 | ||||
|     .item.core-login-site-list-title { | ||||
|         ion-label, ion-label h2.item-heading { | ||||
|             margin-top: 0; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @include media-breakpoint-up(md) { | ||||
|         .scroll-content > * { | ||||
|             max-width: 600px; | ||||
|             margin: 0 auto; | ||||
|             width: 100%; | ||||
|         } | ||||
|         .core-login-site-logo { | ||||
|             margin-top: 20%; | ||||
|         } | ||||
| 
 | ||||
|         &.hidden { | ||||
|             margin: 0; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .core-login-entered-site { | ||||
|         background-color: $gray-lighter; | ||||
|         ion-thumbnail { | ||||
|             box-shadow: 0 0 4px #ddd; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     .core-login-default-icon { | ||||
|         filter: grayscale(100%); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -13,36 +13,26 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, ViewChild, ElementRef } from '@angular/core'; | ||||
| import { IonicPage, NavController, ModalController, NavParams } from 'ionic-angular'; | ||||
| import { IonicPage, NavController, ModalController, AlertController, NavParams } from 'ionic-angular'; | ||||
| import { CoreAppProvider } from '@providers/app'; | ||||
| import { CoreEventsProvider } from '@providers/events'; | ||||
| import { CoreSitesProvider, CoreSiteCheckResponse } from '@providers/sites'; | ||||
| import { CoreSitesProvider, CoreSiteCheckResponse, CoreLoginSiteInfo } from '@providers/sites'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { CoreUrlUtilsProvider } from '@providers/utils/url'; | ||||
| import { CoreConfigConstants } from '../../../../configconstants'; | ||||
| import { CoreLoginHelperProvider } from '../../providers/helper'; | ||||
| import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | ||||
| import { FormBuilder, FormGroup, ValidatorFn, AbstractControl } from '@angular/forms'; | ||||
| import { CoreUrl } from '@singletons/url'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| 
 | ||||
| /** | ||||
|  * Data about an error when connecting to a site. | ||||
|  * Extended data for UI implementation. | ||||
|  */ | ||||
| type CoreLoginSiteError = { | ||||
|     /** | ||||
|      * The error message that ocurred. | ||||
|      */ | ||||
|     message: string; | ||||
| 
 | ||||
|     /** | ||||
|      * URL the user entered. | ||||
|      */ | ||||
|     url?: string; | ||||
| 
 | ||||
|     /** | ||||
|      * URL the user entered with protocol added if needed. | ||||
|      */ | ||||
|     fullUrl?: string; | ||||
| type CoreLoginSiteInfoExtended = CoreLoginSiteInfo & { | ||||
|     fromWS?: boolean; // If the site came from the WS call.
 | ||||
|     noProtocolUrl?: string; // Url wihtout protocol.
 | ||||
|     country?: string; // Based on countrycode.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
| @ -58,12 +48,16 @@ export class CoreLoginSitePage { | ||||
|     @ViewChild('siteFormEl') formElement: ElementRef; | ||||
| 
 | ||||
|     siteForm: FormGroup; | ||||
|     fixedSites: any[]; | ||||
|     filteredSites: any[]; | ||||
|     fixedSites: CoreLoginSiteInfo[]; | ||||
|     filteredSites: CoreLoginSiteInfo[]; | ||||
|     fixedDisplay = 'buttons'; | ||||
|     showKeyboard = false; | ||||
|     filter = ''; | ||||
|     error: CoreLoginSiteError; | ||||
|     sites: CoreLoginSiteInfoExtended[] = []; | ||||
|     hasSites = false; | ||||
|     loadingSites = false; | ||||
|     onlyWrittenSite = false; | ||||
|     searchFnc: Function; | ||||
| 
 | ||||
|     constructor(navParams: NavParams, | ||||
|             protected navCtrl: NavController, | ||||
| @ -72,10 +66,12 @@ export class CoreLoginSitePage { | ||||
|             protected sitesProvider: CoreSitesProvider, | ||||
|             protected loginHelper: CoreLoginHelperProvider, | ||||
|             protected modalCtrl: ModalController, | ||||
|             protected alertCtrl: AlertController, | ||||
|             protected urlUtils: CoreUrlUtilsProvider, | ||||
|             protected domUtils: CoreDomUtilsProvider, | ||||
|             protected eventsProvider: CoreEventsProvider, | ||||
|             protected translate: TranslateService, | ||||
|             protected urlUtils: CoreUrlUtilsProvider) { | ||||
|             protected utils: CoreUtilsProvider) { | ||||
| 
 | ||||
|         this.showKeyboard = !!navParams.get('showKeyboard'); | ||||
| 
 | ||||
| @ -94,8 +90,44 @@ export class CoreLoginSitePage { | ||||
|         } | ||||
| 
 | ||||
|         this.siteForm = fb.group({ | ||||
|             siteUrl: [url, Validators.required] | ||||
|             siteUrl: [url, this.moodleUrlValidator()] | ||||
|         }); | ||||
| 
 | ||||
|         this.searchFnc = this.utils.debounce(async (search: string, isValid: boolean = false) => { | ||||
|             search = search.trim(); | ||||
| 
 | ||||
|             if (search.length >= 3) { | ||||
|                 this.onlyWrittenSite = false; | ||||
| 
 | ||||
|                 // Update the sites list.
 | ||||
|                 this.sites = await this.sitesProvider.findSites(search); | ||||
| 
 | ||||
|                 // UI tweaks.
 | ||||
|                 this.sites.forEach((site) => { | ||||
|                     site.noProtocolUrl = CoreUrl.removeProtocol(site.url); | ||||
|                     site.fromWS = true; | ||||
|                     site.country = this.utils.getCountryName(site.countrycode); | ||||
|                 }); | ||||
| 
 | ||||
|                 // If it's a valid URL, add it.
 | ||||
|                 if (isValid) { | ||||
|                     this.onlyWrittenSite = !!this.sites.length; | ||||
|                     this.sites.unshift({ | ||||
|                         url: search, | ||||
|                         fromWS: false, | ||||
|                         name: this.translate.instant('core.login.yourenteredsite'), | ||||
|                         noProtocolUrl: CoreUrl.removeProtocol(search), | ||||
|                     }); | ||||
|                 } | ||||
| 
 | ||||
|                 this.hasSites = !!this.sites.length; | ||||
|             } else { | ||||
|                 // Not reseting the array to allow animation to be displayed.
 | ||||
|                 this.hasSites = false; | ||||
|             } | ||||
| 
 | ||||
|             this.loadingSites = false; | ||||
|         }, 1000); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -103,8 +135,9 @@ export class CoreLoginSitePage { | ||||
|      * | ||||
|      * @param e Event. | ||||
|      * @param url The URL to connect to. | ||||
|      * @param foundSite The site clicked, if any, from the found sites list. | ||||
|      */ | ||||
|     connect(e: Event, url: string): void { | ||||
|     connect(e: Event, url: string, foundSite?: CoreLoginSiteInfoExtended): void { | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
| 
 | ||||
| @ -130,8 +163,6 @@ export class CoreLoginSitePage { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.hideLoginIssue(); | ||||
| 
 | ||||
|         const modal = this.domUtils.showModalLoading(), | ||||
|             siteData = this.sitesProvider.getDemoSiteData(url); | ||||
| 
 | ||||
| @ -167,7 +198,7 @@ export class CoreLoginSitePage { | ||||
| 
 | ||||
|                     return domain ? this.sitesProvider.checkSite(domain) : Promise.reject(error); | ||||
|                 }) | ||||
|                 .then((result) => this.login(result)) | ||||
|                 .then((result) => this.login(result, foundSite)) | ||||
|                 .catch((error) => this.showLoginIssue(url, error)) | ||||
|                 .finally(() => modal.dismiss()); | ||||
|         } | ||||
| @ -197,13 +228,6 @@ export class CoreLoginSitePage { | ||||
|         modal.present(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Hide the login error. | ||||
|      */ | ||||
|     protected hideLoginIssue(): void { | ||||
|         this.error = null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Show an error that aims people to solve the issue. | ||||
|      * | ||||
| @ -211,13 +235,60 @@ export class CoreLoginSitePage { | ||||
|      * @param error Error to display. | ||||
|      */ | ||||
|     protected showLoginIssue(url: string, error: any): void { | ||||
|         this.error = { | ||||
|             url: url, | ||||
|             message: this.domUtils.getErrorMessage(error), | ||||
|         }; | ||||
|         error = this.domUtils.getErrorMessage(error); | ||||
| 
 | ||||
|         if (error == this.translate.instant('core.cannotconnecttrouble')) { | ||||
|             const found = this.sites.find((site) => site.fromWS && site.url == url); | ||||
| 
 | ||||
|             if (!found) { | ||||
|                 error += ' ' + this.translate.instant('core.cannotconnectverify'); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         let message = '<p>' + error + '</p>'; | ||||
|         if (url) { | ||||
|             this.error.fullUrl = this.urlUtils.isAbsoluteURL(url) ? url : 'https://' + url; | ||||
|             const fullUrl = this.urlUtils.isAbsoluteURL(url) ? url : 'https://' + url; | ||||
|             message += '<p padding><a href="' + fullUrl + '" core-link>' + url + '</a></p>'; | ||||
|         } | ||||
| 
 | ||||
|         const buttons = [ | ||||
|             { | ||||
|                 text: this.translate.instant('core.needhelp'), | ||||
|                 handler: (): void => { | ||||
|                     this.showHelp(); | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 text: this.translate.instant('core.tryagain'), | ||||
|                 role: 'cancel' | ||||
|             } | ||||
|         ]; | ||||
| 
 | ||||
|         this.domUtils.showAlertWithButtons(this.translate.instant('core.cannotconnect'), message, buttons); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Find a site on the backend. | ||||
|      * | ||||
|      * @param e Event. | ||||
|      * @param search Text to search. | ||||
|      */ | ||||
|     searchSite(e: Event, search: string): void { | ||||
|         this.loadingSites = true; | ||||
| 
 | ||||
|         this.searchFnc(search.trim(), this.siteForm.valid); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the demo data for a certain "name" if it is a demo site. | ||||
|      * | ||||
|      * @param name Name of the site to check. | ||||
|      * @return Site data if it's a demo site, undefined otherwise. | ||||
|      */ | ||||
|     getDemoSiteData(name: string): any { | ||||
|         const demoSites = CoreConfigConstants.demo_sites; | ||||
|         if (typeof demoSites != 'undefined' && typeof demoSites[name] != 'undefined') { | ||||
|             return demoSites[name]; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -225,10 +296,11 @@ export class CoreLoginSitePage { | ||||
|      * Process login to a site. | ||||
|      * | ||||
|      * @param response Response obtained from the site check request. | ||||
|      * @param foundSite The site clicked, if any, from the found sites list. | ||||
|      * | ||||
|      * @return Promise resolved after logging in. | ||||
|      */ | ||||
|     protected async login(response: CoreSiteCheckResponse): Promise<void> { | ||||
|     protected async login(response: CoreSiteCheckResponse, foundSite?: CoreLoginSiteInfoExtended): Promise<void> { | ||||
|         return this.sitesProvider.checkRequiredMinimumVersion(response.config).then(() => { | ||||
| 
 | ||||
|             this.domUtils.triggerFormSubmittedEvent(this.formElement, true); | ||||
| @ -242,11 +314,39 @@ export class CoreLoginSitePage { | ||||
|                 this.loginHelper.confirmAndOpenBrowserForSSOLogin( | ||||
|                     response.siteUrl, response.code, response.service, response.config && response.config.launchurl); | ||||
|             } else { | ||||
|                 this.navCtrl.push('CoreLoginCredentialsPage', { siteUrl: response.siteUrl, siteConfig: response.config }); | ||||
|                 const pageParams = { siteUrl: response.siteUrl, siteConfig: response.config }; | ||||
|                 if (foundSite) { | ||||
|                     pageParams['siteName'] = foundSite.name; | ||||
|                     pageParams['logoUrl'] = foundSite.imageurl; | ||||
|                 } | ||||
| 
 | ||||
|                 this.navCtrl.push('CoreLoginCredentialsPage', pageParams); | ||||
|             } | ||||
|         }).catch(() => { | ||||
|             // Ignore errors.
 | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Validate Url. | ||||
|      * | ||||
|      * @return {ValidatorFn} Validation results. | ||||
|      */ | ||||
|     protected moodleUrlValidator(): ValidatorFn { | ||||
|       return (control: AbstractControl): {[key: string]: any} | null => { | ||||
|         const value = control.value.trim(); | ||||
|         let valid = value.length >= 3 && CoreUrl.isValidMoodleUrl(value); | ||||
| 
 | ||||
|         if (!valid) { | ||||
|             const demo = !!this.getDemoSiteData(value); | ||||
| 
 | ||||
|             if (demo) { | ||||
|                 valid = true; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return valid ? null : {siteUrl: {value: control.value}}; | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -12,7 +12,9 @@ | ||||
|     "back": "Back", | ||||
|     "browser": "Browser", | ||||
|     "cancel": "Cancel", | ||||
|     "cannotconnect": "<strong>Cannot connect</strong>: Verify that you have correctly typed your site address.", | ||||
|     "cannotconnect": "Cannot connect", | ||||
|     "cannotconnecttrouble": "We're having trouble connecting to your site.", | ||||
|     "cannotconnectverify": "<strong>Please check the address is correct.</strong>", | ||||
|     "cannotdownloadfiles": "File downloading is disabled. Please contact your site administrator.", | ||||
|     "captureaudio": "Record audio", | ||||
|     "capturedimage": "Taken picture.", | ||||
|  | ||||
| @ -165,6 +165,41 @@ export interface CoreSiteSchema { | ||||
|     migrate?(db: SQLiteDB, oldVersion: number, siteId: string): Promise<any> | void; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Data about sites to be listed. | ||||
|  */ | ||||
| export interface  CoreLoginSiteInfo { | ||||
|     /** | ||||
|      * Site name. | ||||
|      */ | ||||
|     name: string; | ||||
| 
 | ||||
|     /** | ||||
|      * Site alias. | ||||
|      */ | ||||
|     alias?: string; | ||||
| 
 | ||||
|     /** | ||||
|      * URL of the site. | ||||
|      */ | ||||
|     url: string; | ||||
| 
 | ||||
|     /** | ||||
|      * Image URL of the site. | ||||
|      */ | ||||
|     imageurl?: string; | ||||
| 
 | ||||
|     /** | ||||
|      * City of the site. | ||||
|      */ | ||||
|     city?: string; | ||||
| 
 | ||||
|     /** | ||||
|      * Countrycode of the site. | ||||
|      */ | ||||
|     countrycode?: string; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Registered site schema. | ||||
|  */ | ||||
| @ -367,10 +402,17 @@ export class CoreSitesProvider { | ||||
|         ] | ||||
|     }; | ||||
| 
 | ||||
|     constructor(logger: CoreLoggerProvider, private http: HttpClient, private sitesFactory: CoreSitesFactoryProvider, | ||||
|             private appProvider: CoreAppProvider, private translate: TranslateService, private urlUtils: CoreUrlUtilsProvider, | ||||
|             private eventsProvider: CoreEventsProvider,  private textUtils: CoreTextUtilsProvider, | ||||
|             private utils: CoreUtilsProvider, private injector: Injector, private wsProvider: CoreWSProvider, | ||||
|     constructor(logger: CoreLoggerProvider, | ||||
|             protected http: HttpClient, | ||||
|             protected sitesFactory: CoreSitesFactoryProvider, | ||||
|             protected appProvider: CoreAppProvider, | ||||
|             protected translate: TranslateService, | ||||
|             protected urlUtils: CoreUrlUtilsProvider, | ||||
|             protected eventsProvider: CoreEventsProvider, | ||||
|             protected textUtils: CoreTextUtilsProvider, | ||||
|             protected utils: CoreUtilsProvider, | ||||
|             protected injector: Injector, | ||||
|             protected wsProvider: CoreWSProvider, | ||||
|             protected domUtils: CoreDomUtilsProvider) { | ||||
|         this.logger = logger.getInstance('CoreSitesProvider'); | ||||
| 
 | ||||
| @ -431,7 +473,7 @@ export class CoreSitesProvider { | ||||
|                     } else if (this.textUtils.getErrorMessageFromError(secondError)) { | ||||
|                         return Promise.reject(secondError); | ||||
|                     } else { | ||||
|                         return this.translate.instant('core.cannotconnect', {$a: CoreSite.MINIMUM_MOODLE_VERSION}); | ||||
|                         return this.translate.instant('core.cannotconnecttrouble'); | ||||
|                     } | ||||
|                 }); | ||||
|             }); | ||||
| @ -523,8 +565,7 @@ export class CoreSitesProvider { | ||||
|                                         error.error = this.translate.instant('core.login.sitehasredirect'); | ||||
|                                     } else { | ||||
|                                         // We can't be sure if there is a redirect or not. Display cannot connect error.
 | ||||
|                                         error.error = this.translate.instant('core.cannotconnect', | ||||
|                                             {$a: CoreSite.MINIMUM_MOODLE_VERSION}); | ||||
|                                         error.error = this.translate.instant('core.cannotconnecttrouble'); | ||||
|                                     } | ||||
| 
 | ||||
|                                     return Promise.reject(error); | ||||
| @ -569,7 +610,7 @@ export class CoreSitesProvider { | ||||
|         return this.http.post(siteUrl + '/login/token.php', {}).timeout(this.wsProvider.getRequestTimeout()).toPromise() | ||||
|                 .catch(() => { | ||||
|             // Default error messages are kinda bad, return our own message.
 | ||||
|             return Promise.reject({error: this.translate.instant('core.cannotconnect', {$a: CoreSite.MINIMUM_MOODLE_VERSION})}); | ||||
|             return Promise.reject({error: this.translate.instant('core.cannotconnecttrouble')}); | ||||
|         }).then((data: any) => { | ||||
| 
 | ||||
|             if (data.errorcode && (data.errorcode == 'enablewsdescription' || data.errorcode == 'requirecorrectaccess')) { | ||||
| @ -611,7 +652,7 @@ export class CoreSitesProvider { | ||||
| 
 | ||||
|         return promise.then((data: any): any => { | ||||
|             if (typeof data == 'undefined') { | ||||
|                 return Promise.reject(this.translate.instant('core.cannotconnect', {$a: CoreSite.MINIMUM_MOODLE_VERSION})); | ||||
|                 return Promise.reject(this.translate.instant('core.cannotconnecttrouble')); | ||||
|             } else { | ||||
|                 if (typeof data.token != 'undefined') { | ||||
|                     return { token: data.token, siteUrl: siteUrl, privateToken: data.privatetoken }; | ||||
| @ -643,7 +684,7 @@ export class CoreSitesProvider { | ||||
|                 } | ||||
|             } | ||||
|         }, () => { | ||||
|             return Promise.reject(this.translate.instant('core.cannotconnect', {$a: CoreSite.MINIMUM_MOODLE_VERSION})); | ||||
|             return Promise.reject(this.translate.instant('core.cannotconnecttrouble')); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| @ -1926,6 +1967,16 @@ export class CoreSitesProvider { | ||||
|                 return {}; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns site info found on the backend. | ||||
|      * | ||||
|      * @param search Searched text. | ||||
|      * @return Site info list. | ||||
|      */ | ||||
|     async findSites(search: string): Promise<CoreLoginSiteInfo[]> { | ||||
|         return []; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class CoreSites extends makeSingleton(CoreSitesProvider) {} | ||||
|  | ||||
| @ -15,7 +15,7 @@ | ||||
| import { Injectable, SimpleChange, ElementRef } from '@angular/core'; | ||||
| import { | ||||
|     LoadingController, Loading, ToastController, Toast, AlertController, Alert, Platform, Content, PopoverController, | ||||
|     ModalController, | ||||
|     ModalController, AlertButton | ||||
| } from 'ionic-angular'; | ||||
| import { DomSanitizer } from '@angular/platform-browser'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| @ -1138,65 +1138,76 @@ export class CoreDomUtilsProvider { | ||||
|      * @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. | ||||
|      * @return Promise resolved with the alert modal. | ||||
|      */ | ||||
|     showAlert(title: string, message: string, buttonText?: string, autocloseTime?: number): Promise<CoreAlert> { | ||||
|     async showAlert(title: string, message: string, buttonText?: string, autocloseTime?: number): Promise<CoreAlert> { | ||||
|         const buttons = [buttonText || this.translate.instant('core.ok')]; | ||||
| 
 | ||||
|         return this.showAlertWithButtons(title, message, buttons, autocloseTime); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Show an alert modal with some buttons. | ||||
|      * | ||||
|      * @param title Title to show. | ||||
|      * @param message Message to show. | ||||
|      * @param buttons Buttons objects or texts. | ||||
|      * @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed. | ||||
|      * @return Promise resolved with the alert modal. | ||||
|      */ | ||||
|     async showAlertWithButtons(title: string, message: string, buttons: (string | AlertButton)[], autocloseTime?: number): | ||||
|             Promise<CoreAlert> { | ||||
|         const hasHTMLTags = this.textUtils.hasHTMLTags(message); | ||||
|         let promise; | ||||
| 
 | ||||
|         if (hasHTMLTags) { | ||||
|             // Format the text.
 | ||||
|             promise = this.textUtils.formatText(message); | ||||
|         } else { | ||||
|             promise = Promise.resolve(message); | ||||
|             message = await this.textUtils.formatText(message); | ||||
|         } | ||||
| 
 | ||||
|         return promise.then((message) => { | ||||
|             const alertId = <string> Md5.hashAsciiStr((title || '') + '#' + (message || '')); | ||||
|         const alertId = <string> Md5.hashAsciiStr((title || '') + '#' + (message || '')); | ||||
| 
 | ||||
|             if (this.displayedAlerts[alertId]) { | ||||
|                 // There's already an alert with the same message and title. Return it.
 | ||||
|                 return this.displayedAlerts[alertId]; | ||||
|             } | ||||
|         if (this.displayedAlerts[alertId]) { | ||||
|             // There's already an alert with the same message and title. Return it.
 | ||||
|             return this.displayedAlerts[alertId]; | ||||
|         } | ||||
| 
 | ||||
|             const alert: CoreAlert = <any> this.alertCtrl.create({ | ||||
|                 title: title, | ||||
|                 message: message, | ||||
|                 buttons: [buttonText || this.translate.instant('core.ok')] | ||||
|             }); | ||||
| 
 | ||||
|             alert.present().then(() => { | ||||
|                 if (hasHTMLTags) { | ||||
|                     // Treat all anchors so they don't override the app.
 | ||||
|                     const alertMessageEl: HTMLElement = alert.pageRef().nativeElement.querySelector('.alert-message'); | ||||
|                     this.treatAnchors(alertMessageEl); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             // Store the alert and remove it when dismissed.
 | ||||
|             this.displayedAlerts[alertId] = alert; | ||||
| 
 | ||||
|             // Define the observables to extend the Alert class. This will allow several callbacks instead of just one.
 | ||||
|             alert.didDismiss = new Subject(); | ||||
|             alert.willDismiss = new Subject(); | ||||
| 
 | ||||
|             // Set the callbacks to trigger an observable event.
 | ||||
|             alert.onDidDismiss((data: any, role: string) => { | ||||
|                 delete this.displayedAlerts[alertId]; | ||||
| 
 | ||||
|                 alert.didDismiss.next({data: data, role: role}); | ||||
|             }); | ||||
| 
 | ||||
|             alert.onWillDismiss((data: any, role: string) => { | ||||
|                 alert.willDismiss.next({data: data, role: role}); | ||||
|             }); | ||||
| 
 | ||||
|             if (autocloseTime > 0) { | ||||
|                 setTimeout(() => { | ||||
|                     alert.dismiss(); | ||||
|                 }, autocloseTime); | ||||
|             } | ||||
| 
 | ||||
|             return alert; | ||||
|         const alert: CoreAlert = <any> this.alertCtrl.create({ | ||||
|             title: title, | ||||
|             message: message, | ||||
|             buttons: buttons, | ||||
|         }); | ||||
| 
 | ||||
|         alert.present().then(() => { | ||||
|             if (hasHTMLTags) { | ||||
|                 // Treat all anchors so they don't override the app.
 | ||||
|                 const alertMessageEl: HTMLElement = alert.pageRef().nativeElement.querySelector('.alert-message'); | ||||
|                 this.treatAnchors(alertMessageEl); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         // Store the alert and remove it when dismissed.
 | ||||
|         this.displayedAlerts[alertId] = alert; | ||||
| 
 | ||||
|         // Define the observables to extend the Alert class. This will allow several callbacks instead of just one.
 | ||||
|         alert.didDismiss = new Subject(); | ||||
|         alert.willDismiss = new Subject(); | ||||
| 
 | ||||
|         // Set the callbacks to trigger an observable event.
 | ||||
|         alert.onDidDismiss((data: any, role: string) => { | ||||
|             delete this.displayedAlerts[alertId]; | ||||
| 
 | ||||
|             alert.didDismiss.next({data: data, role: role}); | ||||
|         }); | ||||
| 
 | ||||
|         alert.onWillDismiss((data: any, role: string) => { | ||||
|             alert.willDismiss.next({data: data, role: role}); | ||||
|         }); | ||||
| 
 | ||||
|         if (autocloseTime > 0) { | ||||
|             setTimeout(() => { | ||||
|                 alert.dismiss(); | ||||
|             }, autocloseTime); | ||||
|         } | ||||
| 
 | ||||
|         return alert; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -225,7 +225,7 @@ export class CoreUrlUtilsProvider { | ||||
|             url = 'https://' + url; | ||||
|         } | ||||
| 
 | ||||
|         // http allways in lowercase.
 | ||||
|         // http always in lowercase.
 | ||||
|         url = url.replace(/^http/i, 'http'); | ||||
|         url = url.replace(/^https/i, 'https'); | ||||
| 
 | ||||
|  | ||||
| @ -119,4 +119,36 @@ export class CoreUrl { | ||||
|         return urlParts && urlParts.domain ? urlParts.domain : null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the pattern to check if the URL is a valid Moodle Url. | ||||
|      * | ||||
|      * @return {RegExp} Desired RegExp. | ||||
|      */ | ||||
|     static getValidMoodleUrlPattern(): RegExp { | ||||
|         // Regular expression based on RFC 3986: https://tools.ietf.org/html/rfc3986#appendix-B.
 | ||||
|         // Improved to not admit spaces.
 | ||||
|         return new RegExp(/^(([^:/?# ]+):)?(\/\/([^/?# ]*))?([^?# ]*)(\?([^#]*))?(#(.*))?$/); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the given url is valid for the app to connect. | ||||
|      * | ||||
|      * @param  {string}  url Url to check. | ||||
|      * @return {boolean}     True if valid, false otherwise. | ||||
|      */ | ||||
|     static isValidMoodleUrl(url: string): boolean { | ||||
|         const patt = CoreUrl.getValidMoodleUrlPattern(); | ||||
| 
 | ||||
|         return patt.test(url.trim()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Removes protocol from the url. | ||||
|      * | ||||
|      * @param url Site url. | ||||
|      * @return Url without protocol. | ||||
|      */ | ||||
|     static removeProtocol(url: string): string { | ||||
|         return url.replace(/^[a-zA-Z]+:\/\//i, ''); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -6,16 +6,6 @@ $core-dark-item-bg-color: $gray-darker !default; | ||||
| $core-dark-item-divider-bg-color: $gray-dark !default; | ||||
| $core-dark-background-color: $black !default; | ||||
| 
 | ||||
| // Login. | ||||
| $core-dark-login-page-background-color: radial-gradient(white, $gray-dark) !default; | ||||
| $core-dark-login-box-background-color: $black !default; | ||||
| $core-dark-login-box-background-border: $core-login-box-background-border !default; | ||||
| $core-dark-login-box-text-color: $core-dark-text-color !default; | ||||
| $core-dark-login-item-inner-background-color: $core-dark-login-box-background-color !default; | ||||
| $core-dark-login-item-background-color: $core-dark-login-box-background-color !default; | ||||
| $core-dark-login-button-outline: $core-login-button-outline !default; | ||||
| $core-dark-login-loading-color: $core-dark-text-color !default; | ||||
| 
 | ||||
| ion-app.app-root { | ||||
|   @include darkmode() { | ||||
|     ion-action-sheet .action-sheet-container .action-sheet-group .action-sheet-button { | ||||
|  | ||||
| @ -184,16 +184,6 @@ $core-button-outline-background-color: $white !default; | ||||
| 
 | ||||
| $core-network-message-height: 16px !default; | ||||
| 
 | ||||
| // Login. | ||||
| $core-login-page-background-color: radial-gradient(white, $gray-light) !default; | ||||
| $core-login-box-background-color: $white !default; | ||||
| $core-login-box-background-border: $gray !default; | ||||
| $core-login-box-text-color: $text-color !default; | ||||
| $core-login-button-outline: false !default; | ||||
| $core-login-loading-color: false !default; | ||||
| $core-login-item-inner-background-color: $white !default; | ||||
| $core-login-item-background-color: $white !default; | ||||
| 
 | ||||
| $core-action-sheet-color: $core-color !default; | ||||
| $core-action-sheet-cancel-color: $danger !default; | ||||
| $core-dark-action-sheet-cancel-color: $danger-dark !default; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user