diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index e090f6a7ab5..c8f19318599 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Logged out" }, diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.html b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.html index 26cab308095..6d5fc9b8dad 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.html +++ b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.html @@ -1,4 +1,10 @@ - + + diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts index fb98825b1b6..5efd2cf9ab4 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts @@ -9,6 +9,7 @@ export interface AnonLayoutWrapperData { pageTitle?: string; pageSubtitle?: string; pageIcon?: Icon; + showReadonlyHostname?: boolean; } @Component({ @@ -20,6 +21,7 @@ export class AnonLayoutWrapperComponent { protected pageTitle: string; protected pageSubtitle: string; protected pageIcon: Icon; + protected showReadonlyHostname: boolean; constructor( private route: ActivatedRoute, @@ -27,6 +29,7 @@ export class AnonLayoutWrapperComponent { ) { this.pageTitle = this.i18nService.t(this.route.snapshot.firstChild.data["pageTitle"]); this.pageSubtitle = this.i18nService.t(this.route.snapshot.firstChild.data["pageSubtitle"]); - this.pageIcon = this.route.snapshot.firstChild.data["pageIcon"]; // don't translate + this.pageIcon = this.route.snapshot.firstChild.data["pageIcon"]; + this.showReadonlyHostname = this.route.snapshot.firstChild.data["showReadonlyHostname"]; } } diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.html b/libs/auth/src/angular/anon-layout/anon-layout.component.html index 1152d2efbbd..4b593d336e1 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.html +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.html @@ -22,7 +22,13 @@ diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.ts b/libs/auth/src/angular/anon-layout/anon-layout.component.ts index 106844fb5aa..6aa644ea7c3 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.ts @@ -1,9 +1,13 @@ import { CommonModule } from "@angular/common"; import { Component, Input } from "@angular/core"; +import { firstValueFrom } from "rxjs"; +import { ClientType } from "@bitwarden/common/enums"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { IconModule, Icon } from "../../../../components/src/icon"; +import { SharedModule } from "../../../../components/src/shared"; import { TypographyModule } from "../../../../components/src/typography"; import { BitwardenLogo } from "../icons/bitwarden-logo.icon"; @@ -11,21 +15,34 @@ import { BitwardenLogo } from "../icons/bitwarden-logo.icon"; standalone: true, selector: "auth-anon-layout", templateUrl: "./anon-layout.component.html", - imports: [IconModule, CommonModule, TypographyModule], + imports: [IconModule, CommonModule, TypographyModule, SharedModule], }) export class AnonLayoutComponent { @Input() title: string; @Input() subtitle: string; @Input() icon: Icon; + @Input() showReadonlyHostname: boolean; protected logo = BitwardenLogo; - protected version: string; - protected year = "2024"; - constructor(private platformUtilsService: PlatformUtilsService) {} + protected year = "2024"; + protected clientType: ClientType; + protected hostname: string; + protected version: string; + + protected showYearAndVersion = true; + + constructor( + private environmentService: EnvironmentService, + private platformUtilsService: PlatformUtilsService, + ) { + this.year = new Date().getFullYear().toString(); + this.clientType = this.platformUtilsService.getClientType(); + this.showYearAndVersion = this.clientType === ClientType.Web; + } async ngOnInit() { - this.year = new Date().getFullYear().toString(); + this.hostname = (await firstValueFrom(this.environmentService.environment$)).getHostname(); this.version = await this.platformUtilsService.getApplicationVersion(); } } diff --git a/libs/auth/src/angular/anon-layout/anon-layout.mdx b/libs/auth/src/angular/anon-layout/anon-layout.mdx index b98523bba18..3b1de735382 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.mdx +++ b/libs/auth/src/angular/anon-layout/anon-layout.mdx @@ -44,9 +44,15 @@ The AnonLayout**Wrapper**Component embeds the AnonLayoutComponent along with the ```html - + + ``` @@ -54,53 +60,60 @@ To implement, the developer does not need to work with the base AnonLayoutCompon devoloper simply uses the AnonLayout**Wrapper**Component in `oss-routing.module.ts` (for Web, for example) to construct the page via routable composition: -```javascript +```typescript // File: oss-routing.module.ts +import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, LockIcon } from "@bitwarden/auth/angular"; { - path: "", - component: AnonLayoutWrapperComponent, // Wrapper component - children: [ - { - path: "sample-route", // replace with your route - children: [ - { - path: "", - component: MyPrimaryComponent, // replace with your component - }, - { - path: "", - component: MySecondaryComponent, // replace with your component (or remove this secondary outlet object entirely if not needed) - outlet: "secondary", - }, - ], - data: { - pageTitle: "logIn", // example of a translation key from messages.json - pageSubtitle: "loginWithMasterPassword", // example of a translation key from messages.json - pageIcon: LockIcon, // example of an icon to pass in - } satisfies AnonLayoutWrapperData, - }, - ], - }, + path: "", + component: AnonLayoutWrapperComponent, // Wrapper component + children: [ + { + path: "sample-route", // replace with your route + children: [ + { + path: "", + component: MyPrimaryComponent, // replace with your component + }, + { + path: "", + component: MySecondaryComponent, // replace with your component (or remove this secondary outlet object entirely if not needed) + outlet: "secondary", + }, + ], + data: { + pageTitle: "logIn", // example of a translation key from messages.json + pageSubtitle: "loginWithMasterPassword", // example of a translation key from messages.json + pageIcon: LockIcon, // example of an icon to pass in + } satisfies AnonLayoutWrapperData, + }, + ], +}, ``` -And if the AnonLayout**Wrapper**Component is already being used in your client's routing module, -then your work will be as simple as just adding another child route under the `children` array. +(Notice that you can optionally add an `outlet: "secondary"` if you want to project secondary +content below the primary content). + +If the AnonLayout**Wrapper**Component is already being used in your client's routing module, then +your work will be as simple as just adding another child route under the `children` array. + +
### Data Properties -In the `oss-routing.module.ts` example above, notice the data properties being passed in: +Routes that use the AnonLayou**tWrapper**Component can take several unique data properties defined +in the `AnonLayoutWrapperData` interface: - For the `pageTitle` and `pageSubtitle` - pass in a translation key from `messages.json`. - For the `pageIcon` - import an icon (of type `Icon`) into the router file and use the icon directly. +- `showReadonlyHostname` - set to `true` if you want to show the hostname in the footer (ex: + "Accessing bitwarden.com") -All 3 of these properties are optional. +All of these properties are optional. -```javascript -import { AnonLayoutWrapperData, LockIcon } from "@bitwarden/auth/angular"; - -// ... +```typescript +import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, LockIcon } from "@bitwarden/auth/angular"; { // ... @@ -108,10 +121,45 @@ import { AnonLayoutWrapperData, LockIcon } from "@bitwarden/auth/angular"; pageTitle: "logIn", pageSubtitle: "loginWithMasterPassword", pageIcon: LockIcon, + showReadonlyHostname: true, } satisfies AnonLayoutWrapperData, } ``` +### Environment Selector + +For some routes, you may want to display the environment selector in the footer of the +AnonLayoutComponent. To do so, add the relevant environment selector (Web or Libs version, depending +on your client) as a component with `outlet: "environment-selector"`. + +```javascript +// File: oss-routing.module.ts +import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, LockIcon } from "@bitwarden/auth/angular"; +import { EnvironmentSelectorComponent } from "./components/environment-selector/environment-selector.component"; + +{ + path: "", + component: AnonLayoutWrapperComponent, + children: [ + { + path: "sample-route", + children: [ + { + path: "", + component: MyPrimaryComponent, + }, + { + path: "", + component: EnvironmentSelectorComponent, // use Web or Libs component depending on your client + outlet: "environment-selector", + }, + ], + // ... + }, + ], +}, +``` + --- diff --git a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts index 103098349a0..82ca846afbf 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts @@ -1,14 +1,20 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; +import { BehaviorSubject } from "rxjs"; +import { ClientType } from "@bitwarden/common/enums"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ButtonModule } from "../../../../components/src/button"; +import { I18nMockService } from "../../../../components/src/utils/i18n-mock.service"; import { LockIcon } from "../icons"; import { AnonLayoutComponent } from "./anon-layout.component"; class MockPlatformUtilsService implements Partial { getApplicationVersion = () => Promise.resolve("Version 2024.1.1"); + getClientType = () => ClientType.Web; } export default { @@ -22,12 +28,31 @@ export default { provide: PlatformUtilsService, useClass: MockPlatformUtilsService, }, + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + accessing: "Accessing", + }); + }, + }, + { + provide: EnvironmentService, + useValue: { + environment$: new BehaviorSubject({ + getHostname() { + return "bitwarden.com"; + }, + }).asObservable(), + }, + }, ], }), ], args: { title: "The Page Title", subtitle: "The subtitle (optional)", + showReadonlyHostname: true, icon: LockIcon, }, } as Meta; @@ -40,7 +65,7 @@ export const WithPrimaryContent: Story = { template: // Projected content (the
) and styling is just a sample and can be replaced with any content/styling. ` - +
Primary Projected Content Area (customizable)
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
@@ -57,7 +82,7 @@ export const WithSecondaryContent: Story = { // Projected content (the
's) and styling is just a sample and can be replaced with any content/styling. // Notice that slot="secondary" is requred to project any secondary content. ` - +
Primary Projected Content Area (customizable)
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
@@ -78,7 +103,7 @@ export const WithLongContent: Story = { template: // Projected content (the
's) and styling is just a sample and can be replaced with any content/styling. ` - +
Primary Projected Content Area (customizable)
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam? Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit.
@@ -100,7 +125,7 @@ export const WithIcon: Story = { template: // Projected content (the
) and styling is just a sample and can be replaced with any content/styling. ` - +
Primary Projected Content Area (customizable)
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?