diff --git a/scripts/langindex.json b/scripts/langindex.json index 4ab49e21e..565b5b0bf 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1932,6 +1932,8 @@ "core.login.errorexampleurl": "local_moodlemobileapp", "core.login.errorqrnoscheme": "local_moodlemobileapp", "core.login.errorupdatesite": "local_moodlemobileapp", + "core.login.exceededloginattempts": "local_moodlemobileapp", + "core.login.exceededloginattemptssupportsubject": "local_moodlemobileapp", "core.login.faqcannotconnectanswer": "local_moodlemobileapp", "core.login.faqcannotconnectquestion": "local_moodlemobileapp", "core.login.faqcannotfindmysiteanswer": "local_moodlemobileapp", diff --git a/src/core/components/tests/icon.test.ts b/src/core/components/tests/icon.test.ts index 0153b35cc..8efe2fcc6 100644 --- a/src/core/components/tests/icon.test.ts +++ b/src/core/components/tests/icon.test.ts @@ -26,11 +26,11 @@ describe('CoreIconComponent', () => { expect(fixture.nativeElement.innerHTML.trim()).not.toHaveLength(0); const icon = fixture.nativeElement.querySelector('ion-icon'); - const name = icon.getAttribute('name') || icon.getAttribute('ng-reflect-name') || ''; + const name = icon?.getAttribute('name') || icon?.getAttribute('ng-reflect-name') || ''; expect(icon).not.toBeNull(); expect(name).toEqual('fa-thumbs-up'); - expect(icon.getAttribute('role')).toEqual('presentation'); + expect(icon?.getAttribute('role')).toEqual('presentation'); }); }); diff --git a/src/core/components/tests/iframe.test.ts b/src/core/components/tests/iframe.test.ts index 75a0c754d..93106369d 100644 --- a/src/core/components/tests/iframe.test.ts +++ b/src/core/components/tests/iframe.test.ts @@ -33,7 +33,7 @@ describe('CoreIframeComponent', () => { const iframe = nativeElement.querySelector('iframe'); expect(iframe).not.toBeNull(); - expect(iframe.src).toEqual('https://moodle.org/'); + expect(iframe?.src).toEqual('https://moodle.org/'); }); }); diff --git a/src/core/components/tests/user-avatar.test.ts b/src/core/components/tests/user-avatar.test.ts index 7b4c5f12a..a8c085b74 100644 --- a/src/core/components/tests/user-avatar.test.ts +++ b/src/core/components/tests/user-avatar.test.ts @@ -27,7 +27,7 @@ describe('CoreUserAvatarComponent', () => { const image = nativeElement.querySelector('img'); expect(image).not.toBeNull(); - expect(image.src).toEqual(document.location.href + 'assets/img/user-avatar.png'); + expect(image?.src).toEqual(document.location.href + 'assets/img/user-avatar.png'); }); }); diff --git a/src/core/directives/tests/format-text.test.ts b/src/core/directives/tests/format-text.test.ts index 265cd218a..843e76742 100644 --- a/src/core/directives/tests/format-text.test.ts +++ b/src/core/directives/tests/format-text.test.ts @@ -67,7 +67,7 @@ describe('CoreFormatTextDirective', () => { // Assert const text = fixture.nativeElement.querySelector('core-format-text'); expect(text).not.toBeNull(); - expect(text.innerHTML).toEqual(sentence); + expect(text?.innerHTML).toEqual(sentence); }); it('should format text', async () => { @@ -83,7 +83,7 @@ describe('CoreFormatTextDirective', () => { // Assert const text = nativeElement.querySelector('core-format-text'); expect(text).not.toBeNull(); - expect(text.textContent).toEqual('Formatted text'); + expect(text?.textContent).toEqual('Formatted text'); expect(CoreFilter.formatText).toHaveBeenCalledTimes(1); expect(CoreFilter.formatText).toHaveBeenCalledWith( @@ -115,7 +115,7 @@ describe('CoreFormatTextDirective', () => { // Assert const text = nativeElement.querySelector('core-format-text'); expect(text).not.toBeNull(); - expect(text.textContent).toEqual('Formatted text'); + expect(text?.textContent).toEqual('Formatted text'); expect(CoreFilterHelper.getFiltersAndFormatText).toHaveBeenCalledTimes(1); expect(CoreFilterHelper.getFiltersAndFormatText).toHaveBeenCalledWith( @@ -153,7 +153,7 @@ describe('CoreFormatTextDirective', () => { // Assert const image = nativeElement.querySelector('img'); expect(image).not.toBeNull(); - expect(image.src).toEqual('file://local-path/'); + expect(image?.src).toEqual('file://local-path/'); expect(CoreSites.getSite).toHaveBeenCalledWith(site.getId()); expect(CoreFilepool.getSrcByUrl).toHaveBeenCalledTimes(1); @@ -171,7 +171,7 @@ describe('CoreFormatTextDirective', () => { ); const anchor = nativeElement.querySelector('a'); - anchor.click(); + anchor?.click(); // Assert expect(CoreContentLinksHelper.handleLink).toHaveBeenCalledTimes(1); diff --git a/src/core/directives/tests/link.test.ts b/src/core/directives/tests/link.test.ts index 891df6111..169713dce 100644 --- a/src/core/directives/tests/link.test.ts +++ b/src/core/directives/tests/link.test.ts @@ -31,7 +31,7 @@ describe('CoreLinkDirective', () => { const anchor = fixture.nativeElement.querySelector('a'); expect(anchor).not.toBeNull(); - expect(anchor.href).toEqual('https://moodle.org/'); + expect(anchor?.href).toEqual('https://moodle.org/'); }); it('should capture clicks', async () => { @@ -46,7 +46,7 @@ describe('CoreLinkDirective', () => { const anchor = nativeElement.querySelector('a'); - anchor.click(); + anchor?.click(); // Assert expect(CoreContentLinksHelper.handleLink).toHaveBeenCalledTimes(1); diff --git a/src/core/directives/update-non-reactive-attributes.ts b/src/core/directives/update-non-reactive-attributes.ts index 6089403a4..b8ce529d7 100644 --- a/src/core/directives/update-non-reactive-attributes.ts +++ b/src/core/directives/update-non-reactive-attributes.ts @@ -27,7 +27,7 @@ import { Directive, ElementRef, OnDestroy, OnInit } from '@angular/core'; }) export class CoreUpdateNonReactiveAttributesDirective implements OnInit, OnDestroy { - protected element: HTMLIonButtonElement; + protected element: HTMLIonButtonElement | HTMLElement; protected mutationObserver: MutationObserver; constructor(element: ElementRef) { @@ -52,7 +52,11 @@ export class CoreUpdateNonReactiveAttributesDirective implements OnInit, OnDestr * @inheritdoc */ async ngOnInit(): Promise { - await this.element.componentOnReady(); + if ('componentOnReady' in this.element) { + // This may be necessary if this is somehow called but Ionic's directives arent. This happens, for example, + // in some tests such as the credentials page. + await this.element.componentOnReady(); + } this.mutationObserver.observe(this.element, { attributes: true, attributeFilter: ['aria-label'] }); } diff --git a/src/core/features/login/components/components.module.ts b/src/core/features/login/components/components.module.ts index e2cdddf84..2ad4127b7 100644 --- a/src/core/features/login/components/components.module.ts +++ b/src/core/features/login/components/components.module.ts @@ -18,9 +18,11 @@ import { CoreLoginSiteOnboardingComponent } from './site-onboarding/site-onboard import { CoreLoginSiteHelpComponent } from './site-help/site-help'; import { CoreLoginSitesComponent } from './sites/sites'; import { CoreLoginMethodsComponent } from './login-methods/login-methods'; +import { CoreLoginExceededAttemptsComponent } from '@features/login/components/exceeded-attempts/exceeded-attempts'; @NgModule({ declarations: [ + CoreLoginExceededAttemptsComponent, CoreLoginSiteOnboardingComponent, CoreLoginSiteHelpComponent, CoreLoginSitesComponent, @@ -30,6 +32,7 @@ import { CoreLoginMethodsComponent } from './login-methods/login-methods'; CoreSharedModule, ], exports: [ + CoreLoginExceededAttemptsComponent, CoreLoginSiteOnboardingComponent, CoreLoginSiteHelpComponent, CoreLoginSitesComponent, diff --git a/src/core/features/login/components/exceeded-attempts/exceeded-attempts.html b/src/core/features/login/components/exceeded-attempts/exceeded-attempts.html new file mode 100644 index 000000000..aea9a4a2d --- /dev/null +++ b/src/core/features/login/components/exceeded-attempts/exceeded-attempts.html @@ -0,0 +1,13 @@ + + + + +

+ +

+ + {{ 'core.contactsupport' | translate }} + +
+
+
diff --git a/src/core/features/login/components/exceeded-attempts/exceeded-attempts.scss b/src/core/features/login/components/exceeded-attempts/exceeded-attempts.scss new file mode 100644 index 000000000..5fc9e13e4 --- /dev/null +++ b/src/core/features/login/components/exceeded-attempts/exceeded-attempts.scss @@ -0,0 +1,9 @@ +:host { + + ion-button { + margin-left: 0; + margin-right: 0; + margin-top: 16px; + } + +} diff --git a/src/core/features/login/components/exceeded-attempts/exceeded-attempts.ts b/src/core/features/login/components/exceeded-attempts/exceeded-attempts.ts new file mode 100644 index 000000000..f1be67bd9 --- /dev/null +++ b/src/core/features/login/components/exceeded-attempts/exceeded-attempts.ts @@ -0,0 +1,53 @@ +// (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, Input, OnInit } from '@angular/core'; +import { CoreSiteConfig } from '@classes/site'; +import { CoreUserSupport } from '@features/user/services/support'; + +@Component({ + selector: 'core-login-exceeded-attempts', + templateUrl: 'exceeded-attempts.html', + styleUrls: ['./exceeded-attempts.scss'], +}) +export class CoreLoginExceededAttemptsComponent implements OnInit { + + @Input() siteUrl!: string; + @Input() siteConfig!: CoreSiteConfig; + @Input() supportSubject?: string; + + canContactSupport = false; + + /** + * @inheritdoc + */ + ngOnInit(): void { + this.canContactSupport = CoreUserSupport.canContactSupport(this.siteConfig); + } + + /** + * Contact site support. + */ + async contactSupport(): Promise { + if (!this.siteConfig) { + throw new Error('Can\'t contact support without config'); + } + + await CoreUserSupport.contact({ + supportPageUrl: CoreUserSupport.getSupportPageUrl(this.siteConfig, this.siteUrl), + subject: this.supportSubject, + }); + } + +} diff --git a/src/core/features/login/lang.json b/src/core/features/login/lang.json index 282dc4d93..a8cc80245 100644 --- a/src/core/features/login/lang.json +++ b/src/core/features/login/lang.json @@ -27,6 +27,8 @@ "errorexampleurl": "The URL https://campus.example.edu is only an example URL, it's not a real site. Please use the URL of your school or organization's site.", "errorqrnoscheme": "This URL isn't a valid login URL.", "errorupdatesite": "An error occurred while updating the site's token.", + "exceededloginattempts": "Need help logging in? Try recovering your password or contact your site support", + "exceededloginattemptssupportsubject": "I can't log in", "faqcannotconnectanswer": "Please, contact your site administrator.", "faqcannotconnectquestion": "I typed my site address correctly but I still can't connect.", "faqcannotfindmysiteanswer": "Have you typed the name correctly? It's also possible that your site is not included in our public sites directory. If you still can't find it, please enter your site address instead.", diff --git a/src/core/features/login/pages/credentials/credentials.html b/src/core/features/login/pages/credentials/credentials.html index 84f969322..a9f727baf 100644 --- a/src/core/features/login/pages/credentials/credentials.html +++ b/src/core/features/login/pages/credentials/credentials.html @@ -30,6 +30,11 @@

{{siteUrl}}

+ + {{ 'core.login.exceededloginattempts' | translate }} + +