mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 16:23:44 +00:00
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>
174 lines
5.6 KiB
TypeScript
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();
|
|
}
|
|
}
|