diff --git a/src/app/app.module.ts b/src/app/app.module.ts index f9c11888eb..dcd8462db2 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -12,7 +12,7 @@ import { PseudoModule } from 'src/locale/i18n.pseudo.component' import { TitleService } from './core/title-service/title.service' import { HttpContentTypeHeaderInterceptor } from './core/http-content-type-header-interceptor/http-content-type-header-interceptor' import { XsrfFallbackInterceptor } from './core/xsrf/xsrf-fallback.interceptor' -import { FirefoxXsrfPreloadInterceptor } from './core/lang-preload/firefox-xsrf-preload.interceptor' +import { XsrfPreloadInterceptor } from './core/xsrf/xsrf-preload.interceptor' import { RetryTransientInterceptor } from './core/http/retry-transient.interceptor' import { HTTP_INTERCEPTORS, @@ -47,10 +47,12 @@ import { FormsModule } from '@angular/forms' ], providers: [ TitleService, - // Firefox-only workaround to ensure XSRF cookie is established before backend calls + // Ensures the XSRF cookie is established before any backend call, on every + // browser: without it the first mutating request goes out with no + // x-xsrf-token header and gets a 403 { provide: HTTP_INTERCEPTORS, - useClass: FirefoxXsrfPreloadInterceptor, + useClass: XsrfPreloadInterceptor, multi: true, }, { diff --git a/src/app/authorize/pages/authorize/authorize.component.spec.ts b/src/app/authorize/pages/authorize/authorize.component.spec.ts index a9d5260cd1..d4624f6015 100644 --- a/src/app/authorize/pages/authorize/authorize.component.spec.ts +++ b/src/app/authorize/pages/authorize/authorize.component.spec.ts @@ -18,6 +18,7 @@ import { TogglzService } from 'src/app/core/togglz/togglz.service' import { TogglzFlag } from 'src/app/types/config.endpoint' import { OauthURLSessionManagerService } from 'src/app/core/oauth-urlsession-manager/oauth-urlsession-manager.service' import { RumJourneyEventService } from 'src/app/rum/service/customEvent.service' +import { InterstitialObservabilityService } from 'src/app/core/login-interstitials-manager/interstitial-observability.service' import { AuthorizeComponent } from './authorize.component' @@ -38,6 +39,7 @@ describe('AuthorizeComponent', () => { let togglzSpy: jasmine.SpyObj let oauthUrlSessionSpy: jasmine.SpyObj let rumSpy: jasmine.SpyObj + let interstitialObservabilitySpy: jasmine.SpyObj let windowMock: any beforeEach(() => { @@ -57,6 +59,11 @@ describe('AuthorizeComponent', () => { rumSpy = jasmine.createSpyObj('RumJourneyEventService', [ 'recordSimpleEvent', ]) + interstitialObservabilitySpy = + jasmine.createSpyObj( + 'InterstitialObservabilityService', + ['shown', 'outcome', 'closed'] + ) windowMock = { outOfRouterNavigation: jasmine.createSpy('outOfRouterNavigation'), @@ -95,6 +102,10 @@ describe('AuthorizeComponent', () => { useValue: oauthUrlSessionSpy, }, { provide: RumJourneyEventService, useValue: rumSpy }, + { + provide: InterstitialObservabilityService, + useValue: interstitialObservabilitySpy, + }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents() @@ -326,6 +337,9 @@ describe('AuthorizeComponent', () => { finish$.next() expect(finishSpy).toHaveBeenCalled() + // The dialog path closes on afterClosed(); here `finish` is the only signal + // the interstitial is over, so without this the journey never ends + expect(interstitialObservabilitySpy.closed).toHaveBeenCalled() }) it('handleRedirect: with interstitial -> shows interstitial instead of redirect', () => { diff --git a/src/app/authorize/pages/authorize/authorize.component.ts b/src/app/authorize/pages/authorize/authorize.component.ts index f296f1363d..91b5a2d90c 100644 --- a/src/app/authorize/pages/authorize/authorize.component.ts +++ b/src/app/authorize/pages/authorize/authorize.component.ts @@ -1,5 +1,5 @@ import { ComponentType } from '@angular/cdk/overlay' -import { Component, Inject, ViewChild } from '@angular/core' +import { Component, Inject, inject, ViewChild } from '@angular/core' import { Observable, forkJoin, of } from 'rxjs' import { filter, @@ -14,6 +14,7 @@ import { PlatformInfo, PlatformInfoService } from 'src/app/cdk/platform-info' import { WINDOW } from 'src/app/cdk/window' import { UserService } from 'src/app/core' import { LoginMainInterstitialsManagerService } from 'src/app/core/login-interstitials-manager/login-main-interstitials-manager.service' +import { InterstitialObservabilityService } from 'src/app/core/login-interstitials-manager/interstitial-observability.service' import { RecordService } from 'src/app/core/record/record.service' import { TogglzService } from 'src/app/core/togglz/togglz.service' import { LegacyOauthRequestInfoForm as RequestInfoForm } from 'src/app/types/request-info-form.endpoint' @@ -50,6 +51,7 @@ export class AuthorizeComponent { redirectByReportAlreadyAuthorize: boolean OAUTH2_AUTHORIZATION_ENABLE: boolean private log: ReturnType + private interstitialObservability = inject(InterstitialObservabilityService) constructor( private userService: UserService, @@ -183,8 +185,13 @@ export class AuthorizeComponent { const componentRef = this.outlet.attachComponentPortal(portal) + // The dialog path closes the journey on afterClosed(); here the host owns + // the component's lifetime, so `finish` is the equivalent end point componentRef.instance.finish - .pipe(switchMap(() => this.finishRedirect())) + .pipe( + tap(() => this.interstitialObservability.closed()), + switchMap(() => this.finishRedirect()) + ) .subscribe() componentRef.changeDetectorRef.detectChanges() diff --git a/src/app/cdk/interstitials/backup-email/interstitial-component/backup-email.component.html b/src/app/cdk/interstitials/backup-email/interstitial-component/backup-email.component.html new file mode 100644 index 0000000000..716f7539e9 --- /dev/null +++ b/src/app/cdk/interstitials/backup-email/interstitial-component/backup-email.component.html @@ -0,0 +1,152 @@ + + + orcid logo + +

+ Add a backup email address +

+
+
+ + + +
+ We’ve noticed you only have one email address associated with your ORCID + account. Adding a backup email will help you sign in to ORCID if you lose + access to your primary email address. +
+
+ +
+

+ Your primary email address +

+ +
+ {{ primaryEmail?.value }} + + +
+ +
+ +
+

+ Your backup email address +

+

+ Add another email to your ORCID account as a backup. A mix of personal and + professional email addresses works best. +

+ +
+
+ + Backup email + + + + + + + Please enter your email + + + Please enter a valid email address, for example + joe@institution.edu + + + This email is already associated with an existing ORCID record. Please + use a different email address. + +
+
+
+ + + + +
+ + +
+
+ + diff --git a/src/app/cdk/interstitials/backup-email/interstitial-component/backup-email.component.scss b/src/app/cdk/interstitials/backup-email/interstitial-component/backup-email.component.scss new file mode 100644 index 0000000000..bf239dbc73 --- /dev/null +++ b/src/app/cdk/interstitials/backup-email/interstitial-component/backup-email.component.scss @@ -0,0 +1,134 @@ +@import '../../../../../assets/scss/orcid.spacing.scss'; + +// The orc-font-* classes set neither line-height nor font-weight, so the Figma +// metrics are spelled out here. + +section { + margin-top: $spacing-large; +} + +h2 { + margin: 0 0 $spacing-base 0; + font-weight: bold; + line-height: 24px; // Figma body-M 16/24 +} + +.primary-email-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: $spacing-small; + + strong { + font-weight: 700; + line-height: 21px; // Figma $body-s 14/21 bold + } +} + +.email-category { + display: flex; + align-items: center; + gap: $spacing-xsmall; + + span { + line-height: 18px; // Figma $body-xs 12/18 + } + + mat-icon { + height: 24px; + width: 24px; + font-size: 24px; + } +} + +.backup-email p { + margin: 0; +} + +.input-container, +mat-form-field { + width: 100%; +} + +// Figma stacks the label, the field and the message with an even 8px gap +.input-container { + margin-top: $spacing-base; + display: flex; + flex-direction: column; + gap: $spacing-small; +} + +mat-label.orc-font-small-print { + font-weight: bold; + line-height: 18px; // Figma $body-xs 12/18 +} + +mat-error.orc-font-small-print { + line-height: 18px; +} + +button.authorize-button { + width: 100%; + margin-bottom: $spacing-base; +} + +// The decline action is a button so it is keyboard reachable, styled as a link +button.link-button { + background: none; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + text-decoration: underline; +} + +mat-divider { + margin-top: $spacing-base; + margin-bottom: $spacing-base; +} + +mat-divider.footer-divider { + margin-top: $spacing-large; + margin-bottom: $spacing-large; +} + +mat-icon > img { + height: 20px; + margin-top: 2px; // Fix icon component with SVG image +} + +mat-spinner { + align-self: center; + margin: 60px; +} + +mat-card-header.authorize-header { + mat-card-title { + margin: 0 0 $spacing-large 0 !important; + } + + h1 { + margin-top: 0; + } + + .orc-font-heading-small { + font-style: normal; + font-weight: 500; + } + + mat-icon.logo-icon { + height: 80px; + width: 64px; + img { + height: 64px; + } + } +} + +mat-card-content.authorize-content { + margin: 0px !important; + + div.deny-button-wrapper { + justify-content: center; + } +} diff --git a/src/app/cdk/interstitials/backup-email/interstitial-component/backup-email.component.scss-theme.scss b/src/app/cdk/interstitials/backup-email/interstitial-component/backup-email.component.scss-theme.scss new file mode 100644 index 0000000000..6009bac994 --- /dev/null +++ b/src/app/cdk/interstitials/backup-email/interstitial-component/backup-email.component.scss-theme.scss @@ -0,0 +1,39 @@ +@use '@angular/material' as mat; +@import 'src/assets/scss/material.orcid-theme.scss'; + +@mixin theme($theme) { + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, accent); + $foreground: map-get($theme, foreground); + $background: map-get($theme, background); + + .mat-mdc-raised-button.mat-primary:not(.mat-raised-button.mat-primary) { + background-color: mat.m2-get-color-from-palette($primary, 700); + } + + // The decline action is a