1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23:44 +00:00
Files
browser/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts
Alec Rippberger 0df7b53bb4 feat(sso): [PM-8114] implement SSO component UI refresh
Consolidates existing SSO components into a single unified component in
libs/auth, matching the new design system. This implementation:

- Creates a new shared SsoComponent with extracted business logic
- Adds feature flag support for unauth-ui-refresh
- Updates page styling including new icons and typography
- Preserves web client claimed domain logic
- Maintains backwards compatibility with legacy views

PM-8114

---------

Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
Co-authored-by: Jared Snider <jsnider@bitwarden.com>
2024-12-12 10:28:30 -06:00

174 lines
5.6 KiB
TypeScript

// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Data, NavigationEnd, Router, RouterModule } from "@angular/router";
import { Subject, filter, switchMap, takeUntil, tap } from "rxjs";
import { AnonLayoutComponent } from "@bitwarden/auth/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Icon, Translation } from "@bitwarden/components";
import { AnonLayoutWrapperDataService } from "./anon-layout-wrapper-data.service";
export interface AnonLayoutWrapperData {
/**
* The optional title of the page.
* If a string is provided, it will be presented as is (ex: Organization name)
* If a Translation object (supports placeholders) is provided, it will be translated
*/
pageTitle?: string | Translation | null;
/**
* The optional subtitle of the page.
* If a string is provided, it will be presented as is (ex: user's email)
* If a Translation object (supports placeholders) is provided, it will be translated
*/
pageSubtitle?: string | Translation | null;
/**
* The optional icon to display on the page.
*/
pageIcon?: Icon | null;
/**
* Optional flag to either show the optional environment selector (false) or just a readonly hostname (true).
*/
showReadonlyHostname?: boolean;
/**
* Optional flag to set the max-width of the page. Defaults to 'md' if not provided.
*/
maxWidth?: "md" | "3xl";
/**
* Optional flag to set the max-width of the title area. Defaults to null if not provided.
*/
titleAreaMaxWidth?: "md";
}
@Component({
standalone: true,
templateUrl: "anon-layout-wrapper.component.html",
imports: [AnonLayoutComponent, RouterModule],
})
export class AnonLayoutWrapperComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
protected pageTitle: string;
protected pageSubtitle: string;
protected pageIcon: Icon;
protected showReadonlyHostname: boolean;
protected maxWidth: "md" | "3xl";
protected titleAreaMaxWidth: "md";
constructor(
private router: Router,
private route: ActivatedRoute,
private i18nService: I18nService,
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
private changeDetectorRef: ChangeDetectorRef,
) {}
ngOnInit(): void {
// Set the initial page data on load
this.setAnonLayoutWrapperDataFromRouteData(this.route.snapshot.firstChild?.data);
// Listen for page changes and update the page data appropriately
this.listenForPageDataChanges();
this.listenForServiceDataChanges();
}
private listenForPageDataChanges() {
this.router.events
.pipe(
filter((event) => event instanceof NavigationEnd),
// reset page data on page changes
tap(() => this.resetPageData()),
switchMap(() => this.route.firstChild?.data || null),
takeUntil(this.destroy$),
)
.subscribe((firstChildRouteData: Data | null) => {
this.setAnonLayoutWrapperDataFromRouteData(firstChildRouteData);
});
}
private setAnonLayoutWrapperDataFromRouteData(firstChildRouteData: Data | null) {
if (!firstChildRouteData) {
return;
}
if (firstChildRouteData["pageTitle"] !== undefined) {
this.pageTitle = this.handleStringOrTranslation(firstChildRouteData["pageTitle"]);
}
if (firstChildRouteData["pageSubtitle"] !== undefined) {
this.pageSubtitle = this.handleStringOrTranslation(firstChildRouteData["pageSubtitle"]);
}
if (firstChildRouteData["pageIcon"] !== undefined) {
this.pageIcon = firstChildRouteData["pageIcon"];
}
this.showReadonlyHostname = Boolean(firstChildRouteData["showReadonlyHostname"]);
this.maxWidth = firstChildRouteData["maxWidth"];
this.titleAreaMaxWidth = firstChildRouteData["titleAreaMaxWidth"];
}
private listenForServiceDataChanges() {
this.anonLayoutWrapperDataService
.anonLayoutWrapperData$()
.pipe(takeUntil(this.destroy$))
.subscribe((data: AnonLayoutWrapperData) => {
this.setAnonLayoutWrapperData(data);
});
}
private setAnonLayoutWrapperData(data: AnonLayoutWrapperData) {
if (!data) {
return;
}
// Null emissions are used to reset the page data as all fields are optional.
if (data.pageTitle !== undefined) {
this.pageTitle =
data.pageTitle !== null ? this.handleStringOrTranslation(data.pageTitle) : null;
}
if (data.pageSubtitle !== undefined) {
this.pageSubtitle =
data.pageSubtitle !== null ? this.handleStringOrTranslation(data.pageSubtitle) : null;
}
if (data.pageIcon !== undefined) {
this.pageIcon = data.pageIcon !== null ? data.pageIcon : null;
}
if (data.showReadonlyHostname !== undefined) {
this.showReadonlyHostname = data.showReadonlyHostname;
}
// Manually fire change detection to avoid ExpressionChangedAfterItHasBeenCheckedError
// when setting the page data from a service
this.changeDetectorRef.detectChanges();
}
private handleStringOrTranslation(value: string | Translation): string {
if (typeof value === "string") {
// If it's a string, return it as is
return value;
}
// If it's a Translation object, translate it
return this.i18nService.t(value.key, ...(value.placeholders ?? []));
}
private resetPageData() {
this.pageTitle = null;
this.pageSubtitle = null;
this.pageIcon = null;
this.showReadonlyHostname = null;
this.maxWidth = null;
this.titleAreaMaxWidth = null;
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}