diff --git a/change/@azure-msal-angular-f9e43c58-0c6f-4239-9cae-ddd37a47e893.json b/change/@azure-msal-angular-f9e43c58-0c6f-4239-9cae-ddd37a47e893.json new file mode 100644 index 0000000000..d53f4e8b50 --- /dev/null +++ b/change/@azure-msal-angular-f9e43c58-0c6f-4239-9cae-ddd37a47e893.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "dx: Fix InjectionToken types to allow new Angular 14+ inject(TOKEN) syntax.", + "packageName": "@azure/msal-angular", + "email": "bradkovach@gmail.com", + "dependentChangeType": "patch" +} diff --git a/lib/msal-angular/src/constants.spec.ts b/lib/msal-angular/src/constants.spec.ts new file mode 100644 index 0000000000..181e6452db --- /dev/null +++ b/lib/msal-angular/src/constants.spec.ts @@ -0,0 +1,50 @@ +import { Component, inject } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { + MSAL_BROADCAST_CONFIG, + MSAL_GUARD_CONFIG, + MSAL_INSTANCE, + MSAL_INTERCEPTOR_CONFIG, +} from "./constants"; + +describe("Injection Tokens", () => { + it("should produce helpful type errors on mismatch", async () => { + // These are all cases where a type error is expected. + // While Jest cannot verify the type errors, we can use TypeScript's + // type checking to verify that the errors are present. + @Component({ + selector: "app-strongly-typed-functional-injection", + template: "", + }) + class MistypedFunctionalInjection { + // @ts-expect-error MSAL_INSTANCE is not a string. + msalInstance: string = inject(MSAL_INSTANCE); + // @ts-expect-error MSAL_GUARD_CONFIG is not a string. + guardConfig: string = inject(MSAL_GUARD_CONFIG); + // @ts-expect-error MSAL_INTERCEPTOR_CONFIG is not a string. + interceptorConfig: string = inject(MSAL_INTERCEPTOR_CONFIG); + // @ts-expect-error MSAL_BROADCAST_CONFIG is not a string. + broadcastConfig: string = inject(MSAL_BROADCAST_CONFIG); + } + + await TestBed.configureTestingModule({ + providers: [ + MSAL_INSTANCE, + MSAL_GUARD_CONFIG, + MSAL_INTERCEPTOR_CONFIG, + MSAL_BROADCAST_CONFIG, + ].map((provide, useValue) => ({ + provide, + useValue, + })), + }).compileComponents(); + + const fixture = TestBed.createComponent(MistypedFunctionalInjection); + const instance = fixture.componentInstance; + + expect(instance.msalInstance).toBeDefined(); + expect(instance.guardConfig).toBeDefined(); + expect(instance.interceptorConfig).toBeDefined(); + expect(instance.broadcastConfig).toBeDefined(); + }); +}); diff --git a/lib/msal-angular/src/constants.ts b/lib/msal-angular/src/constants.ts index dbb44746a5..5d79d6f0e1 100644 --- a/lib/msal-angular/src/constants.ts +++ b/lib/msal-angular/src/constants.ts @@ -5,16 +5,21 @@ import { InjectionToken } from "@angular/core"; -export const MSAL_INSTANCE = new InjectionToken("MSAL_INSTANCE"); +import { type IPublicClientApplication } from "@azure/msal-browser"; +import { type MsalBroadcastConfiguration } from "./msal.broadcast.config"; +import { type MsalGuardConfiguration } from "./msal.guard.config"; +import { type MsalInterceptorConfiguration } from "./msal.interceptor.config"; -export const MSAL_GUARD_CONFIG = new InjectionToken( - "MSAL_GUARD_CONFIG" +export const MSAL_INSTANCE = new InjectionToken( + "MSAL_INSTANCE" ); -export const MSAL_INTERCEPTOR_CONFIG = new InjectionToken( - "MSAL_INTERCEPTOR_CONFIG" +export const MSAL_GUARD_CONFIG = new InjectionToken( + "MSAL_GUARD_CONFIG" ); -export const MSAL_BROADCAST_CONFIG = new InjectionToken( - "MSAL_BROADCAST_CONFIG" -); +export const MSAL_INTERCEPTOR_CONFIG = + new InjectionToken("MSAL_INTERCEPTOR_CONFIG"); + +export const MSAL_BROADCAST_CONFIG = + new InjectionToken("MSAL_BROADCAST_CONFIG"); diff --git a/samples/msal-angular-samples/angular-standalone-sample/src/app/app.component.ts b/samples/msal-angular-samples/angular-standalone-sample/src/app/app.component.ts index fe09468787..7160888440 100644 --- a/samples/msal-angular-samples/angular-standalone-sample/src/app/app.component.ts +++ b/samples/msal-angular-samples/angular-standalone-sample/src/app/app.component.ts @@ -1,5 +1,5 @@ -import { Component, OnInit, Inject, OnDestroy } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { Component, DestroyRef, inject, OnInit } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatMenuModule } from '@angular/material/menu'; import { MatButtonModule } from '@angular/material/button'; import { MatToolbarModule } from '@angular/material/toolbar'; @@ -19,38 +19,34 @@ import { EventMessage, EventType, } from '@azure/msal-browser'; -import { Subject } from 'rxjs'; -import { filter, takeUntil } from 'rxjs/operators'; +import { filter } from 'rxjs/operators'; @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.css'], - imports: [ - CommonModule, - MsalModule, - RouterOutlet, - RouterLink, - MatToolbarModule, - MatButtonModule, - MatMenuModule, - ] + selector: 'app-root', + templateUrl: './app.component.html', + styleUrl: './app.component.css', + imports: [ + MsalModule, + RouterOutlet, + RouterLink, + MatToolbarModule, + MatButtonModule, + MatMenuModule, + ] }) -export class AppComponent implements OnInit, OnDestroy { +export class AppComponent implements OnInit { + private destroyRef = inject(DestroyRef); + private msalGuardConfig = inject(MSAL_GUARD_CONFIG); + private authService = inject(MsalService); + private msalBroadcastService = inject(MsalBroadcastService); + title = 'Angular Standalone Sample - MSAL Angular'; isIframe = false; loginDisplay = false; - private readonly _destroying$ = new Subject(); - - constructor( - @Inject(MSAL_GUARD_CONFIG) private msalGuardConfig: MsalGuardConfiguration, - private authService: MsalService, - private msalBroadcastService: MsalBroadcastService - ) {} ngOnInit(): void { this.authService.handleRedirectObservable().subscribe(); - + this.isIframe = window !== window.parent && !window.opener; // Remove this line to use Angular Universal this.msalBroadcastService.msalSubject$ @@ -74,7 +70,7 @@ export class AppComponent implements OnInit, OnDestroy { filter( (status: InteractionStatus) => status === InteractionStatus.None ), - takeUntil(this._destroying$) + takeUntilDestroyed(this.destroyRef) ) .subscribe(() => { this.setLoginDisplay(); @@ -138,9 +134,4 @@ export class AppComponent implements OnInit, OnDestroy { this.authService.logoutRedirect(); } } - - ngOnDestroy(): void { - this._destroying$.next(undefined); - this._destroying$.complete(); - } }