mirror of
https://github.com/bitwarden/browser
synced 2026-01-26 14:23:46 +00:00
Merge branch 'main' into billing/pm-29600/update-tax-client
This commit is contained in:
@@ -1565,7 +1565,6 @@ export default class MainBackground {
|
||||
await this.sdkLoadService.loadAndInit();
|
||||
// Only the "true" background should run migrations
|
||||
await this.migrationRunner.run();
|
||||
this.encryptService.init(this.configService);
|
||||
|
||||
// This is here instead of in the InitService b/c we don't plan for
|
||||
// side effects to run in the Browser InitService.
|
||||
|
||||
@@ -2,8 +2,6 @@ import { inject, Inject, Injectable, DOCUMENT } from "@angular/core";
|
||||
|
||||
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -30,8 +28,6 @@ export class InitService {
|
||||
private sdkLoadService: SdkLoadService,
|
||||
private viewCacheService: PopupViewCacheService,
|
||||
private readonly migrationRunner: MigrationRunner,
|
||||
private configService: ConfigService,
|
||||
private encryptService: EncryptService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
) {}
|
||||
|
||||
@@ -43,7 +39,6 @@ export class InitService {
|
||||
this.twoFactorService.init();
|
||||
await this.viewCacheService.init();
|
||||
await this.sizeService.init();
|
||||
this.encryptService.init(this.configService);
|
||||
|
||||
const htmlEl = window.document.documentElement;
|
||||
this.themingService.applyThemeChangesTo(this.document);
|
||||
|
||||
@@ -1058,7 +1058,6 @@ export class ServiceContainer {
|
||||
this.containerService.attachToGlobal(global);
|
||||
await this.i18nService.init();
|
||||
this.twoFactorService.init();
|
||||
this.encryptService.init(this.configService);
|
||||
|
||||
// If a user has a BW_SESSION key stored in their env (not process.env.BW_SESSION),
|
||||
// this should set the user key to unlock the vault on init.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<bit-layout class="!tw-h-full" rounded>
|
||||
<bit-layout class="!tw-h-full">
|
||||
<app-side-nav slot="side-nav">
|
||||
<bit-nav-logo [openIcon]="logo" route="." [label]="'passwordManager' | i18n" />
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { DefaultVaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
@@ -54,7 +53,6 @@ export class InitService {
|
||||
private autotypeService: DesktopAutotypeService,
|
||||
private sdkLoadService: SdkLoadService,
|
||||
private biometricMessageHandlerService: BiometricMessageHandlerService,
|
||||
private configService: ConfigService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
private readonly migrationRunner: MigrationRunner,
|
||||
) {}
|
||||
@@ -65,7 +63,6 @@ export class InitService {
|
||||
await this.sshAgentService.init();
|
||||
this.nativeMessagingService.init();
|
||||
await this.migrationRunner.waitForCompletion(); // Desktop will run migrations in the main process
|
||||
this.encryptService.init(this.configService);
|
||||
|
||||
const accounts = await firstValueFrom(this.accountService.accounts$);
|
||||
const setUserKeyInMemoryPromises = [];
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
buttonType="primary"
|
||||
(click)="addSendWithoutType()"
|
||||
>
|
||||
{{ "newSend" | i18n }}
|
||||
{{ "createSend" | i18n }}
|
||||
</button>
|
||||
</tools-send-list>
|
||||
</div>
|
||||
|
||||
@@ -79,7 +79,6 @@ export class SendV2Component {
|
||||
|
||||
protected readonly sendId = signal<string | null>(null);
|
||||
protected readonly action = signal<Action>(Action.None);
|
||||
private readonly selectedSendTypeOverride = signal<SendType | undefined>(undefined);
|
||||
|
||||
private sendFormConfigService = inject(DefaultSendFormConfigService);
|
||||
private sendItemsService = inject(SendItemsService);
|
||||
@@ -151,10 +150,9 @@ export class SendV2Component {
|
||||
|
||||
protected readonly selectedSendType = computed(() => {
|
||||
const action = this.action();
|
||||
const typeOverride = this.selectedSendTypeOverride();
|
||||
|
||||
if (action === Action.Add && typeOverride !== undefined) {
|
||||
return typeOverride;
|
||||
if (action === Action.Add) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sendId = this.sendId();
|
||||
@@ -173,24 +171,20 @@ export class SendV2Component {
|
||||
} else {
|
||||
this.action.set(Action.Add);
|
||||
this.sendId.set(null);
|
||||
this.selectedSendTypeOverride.set(type);
|
||||
|
||||
const component = this.addEditComponent();
|
||||
if (component) {
|
||||
await component.resetAndLoad();
|
||||
}
|
||||
this.cdr.detectChanges();
|
||||
void this.addEditComponent()?.resetAndLoad();
|
||||
}
|
||||
}
|
||||
|
||||
/** Used by old UI to add a send without specifying type (defaults to Text) */
|
||||
/** Used by old UI to add a send without specifying type (defaults to File) */
|
||||
protected async addSendWithoutType(): Promise<void> {
|
||||
await this.addSend(SendType.Text);
|
||||
await this.addSend(SendType.File);
|
||||
}
|
||||
|
||||
protected closeEditPanel(): void {
|
||||
this.action.set(Action.None);
|
||||
this.sendId.set(null);
|
||||
this.selectedSendTypeOverride.set(undefined);
|
||||
}
|
||||
|
||||
protected async savedSend(send: SendView): Promise<void> {
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
<bit-nav-group icon="bwi-vault" [text]="'vault' | i18n" route="new-vault">
|
||||
<app-organization-filter
|
||||
[activeFilter]="activeFilter()"
|
||||
[organizations]="organizations$ | async"
|
||||
[organizations]="organizations()"
|
||||
[activeOrganizationDataOwnership]="activeOrganizationDataOwnershipPolicy"
|
||||
[activeSingleOrganizationPolicy]="activeSingleOrganizationPolicy"
|
||||
/>
|
||||
<app-type-filter [activeFilter]="activeFilter()" [cipherTypes]="cipherTypes$ | async" />
|
||||
<app-type-filter [activeFilter]="activeFilter()" [cipherTypes]="cipherTypes()" />
|
||||
<app-status-filter [hideArchive]="!showArchiveVaultFilter" [activeFilter]="activeFilter()" />
|
||||
@if (showCollectionsFilter()) {
|
||||
<bit-nav-group
|
||||
@@ -20,7 +20,7 @@
|
||||
[appA11yTitle]="'collections' | i18n"
|
||||
[disableToggleOnClick]="true"
|
||||
>
|
||||
@for (collection of (collections$ | async)?.children ?? []; track collection.node.id) {
|
||||
@for (collection of collections()?.children ?? []; track collection.node.id) {
|
||||
<app-collection-filter [activeFilter]="activeFilter()" [collection]="collection" />
|
||||
}
|
||||
</bit-nav-group>
|
||||
@@ -32,7 +32,7 @@
|
||||
[appA11yTitle]="'folders' | i18n"
|
||||
[disableToggleOnClick]="true"
|
||||
>
|
||||
@for (folder of (folders$ | async)?.children ?? []; track folder.node.id) {
|
||||
@for (folder of folders()?.children ?? []; track folder.node.id) {
|
||||
<app-folder-filter
|
||||
[activeFilter]="activeFilter()"
|
||||
[folder]="folder"
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, inject, OnInit, output, computed, signal } from "@angular/core";
|
||||
import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs";
|
||||
import { toSignal } from "@angular/core/rxjs-interop";
|
||||
import { firstValueFrom, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
@@ -12,13 +13,9 @@ import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { NavigationModule, DialogService, A11yTitleDirective } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import {
|
||||
OrganizationFilter,
|
||||
CipherTypeFilter,
|
||||
CollectionFilter,
|
||||
FolderFilter,
|
||||
VaultFilter,
|
||||
VaultFilterServiceAbstraction as VaultFilterService,
|
||||
@@ -75,13 +72,25 @@ export class VaultFilterComponent implements OnInit {
|
||||
protected showArchiveVaultFilter = false;
|
||||
protected activeOrganizationDataOwnershipPolicy: boolean;
|
||||
protected activeSingleOrganizationPolicy: boolean;
|
||||
protected organizations$: Observable<TreeNode<OrganizationFilter>>;
|
||||
protected collections$: Observable<TreeNode<CollectionFilter>>;
|
||||
protected folders$: Observable<TreeNode<FolderFilter>>;
|
||||
protected cipherTypes$: Observable<TreeNode<CipherTypeFilter>>;
|
||||
protected readonly organizations = toSignal(this.vaultFilterService.organizationTree$);
|
||||
protected readonly collections = toSignal(this.vaultFilterService.collectionTree$);
|
||||
protected readonly folders = toSignal(this.vaultFilterService.folderTree$);
|
||||
protected readonly cipherTypes = toSignal(this.vaultFilterService.cipherTypeTree$);
|
||||
|
||||
protected readonly showCollectionsFilter = computed<boolean>(() => {
|
||||
return this.organizations$ != null && !this.activeFilter()?.isMyVaultSelected;
|
||||
return (
|
||||
this.organizations() != null &&
|
||||
!this.activeFilter()?.isMyVaultSelected &&
|
||||
!this.allOrganizationsDisabled()
|
||||
);
|
||||
});
|
||||
|
||||
protected readonly allOrganizationsDisabled = computed<boolean>(() => {
|
||||
if (!this.organizations()) {
|
||||
return false;
|
||||
}
|
||||
const orgs = this.organizations().children.filter((org) => org.node.id !== "MyVault");
|
||||
return orgs.length > 0 && orgs.every((org) => !org.node.enabled);
|
||||
});
|
||||
|
||||
private async setActivePolicies() {
|
||||
@@ -98,16 +107,9 @@ export class VaultFilterComponent implements OnInit {
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.organizations$ = this.vaultFilterService.organizationTree$;
|
||||
if (
|
||||
this.organizations$ != null &&
|
||||
(await firstValueFrom(this.organizations$)).children.length > 0
|
||||
) {
|
||||
if (this.organizations() != null && this.organizations().children.length > 0) {
|
||||
await this.setActivePolicies();
|
||||
}
|
||||
this.cipherTypes$ = this.vaultFilterService.cipherTypeTree$;
|
||||
this.folders$ = this.vaultFilterService.folderTree$;
|
||||
this.collections$ = this.vaultFilterService.collectionTree$;
|
||||
|
||||
this.showArchiveVaultFilter = await firstValueFrom(
|
||||
this.cipherArchiveService.hasArchiveFlagEnabled$,
|
||||
|
||||
@@ -805,6 +805,8 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener {
|
||||
type: CipherViewLikeUtils.getType(cipher),
|
||||
// Normalize undefined organizationId to null for filter compatibility
|
||||
organizationId: cipher.organizationId ?? null,
|
||||
// Normalize empty string folderId to null for filter compatibility
|
||||
folderId: cipher.folderId ? cipher.folderId : null,
|
||||
// Explicitly include isDeleted and isArchived since they might be getters
|
||||
isDeleted: CipherViewLikeUtils.isDeleted(cipher),
|
||||
isArchived: CipherViewLikeUtils.isArchived(cipher),
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
(click)="invite(organization)"
|
||||
[disabled]="!firstLoaded()"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-plus bwi-fw tw-me-2" aria-hidden="true"></i>
|
||||
{{ "inviteMember" | i18n }}
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { DefaultVaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { IpcService } from "@bitwarden/common/platform/ipc";
|
||||
@@ -40,7 +39,6 @@ export class InitService {
|
||||
private ipcService: IpcService,
|
||||
private sdkLoadService: SdkLoadService,
|
||||
private taskService: TaskService,
|
||||
private configService: ConfigService,
|
||||
private readonly migrationRunner: MigrationRunner,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
) {}
|
||||
@@ -49,7 +47,6 @@ export class InitService {
|
||||
return async () => {
|
||||
await this.sdkLoadService.loadAndInit();
|
||||
await this.migrationRunner.run();
|
||||
this.encryptService.init(this.configService);
|
||||
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
if (activeAccount) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<button bitButton [bitMenuTriggerFor]="itemOptions" buttonType="primary" type="button">
|
||||
<i *ngIf="!hideIcon" class="bwi bwi-plus" aria-hidden="true"></i>
|
||||
<i *ngIf="!hideIcon" class="bwi bwi-plus tw-me-2" aria-hidden="true"></i>
|
||||
{{ (hideIcon ? "createSend" : "new") | i18n }}
|
||||
</button>
|
||||
<bit-menu #itemOptions>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</h3>
|
||||
|
||||
<p bitTypography="body1" class="tw-mb-6 tw-max-w-sm">
|
||||
{{ "sendCreatedDescription" | i18n: formattedExpirationTime }}
|
||||
{{ "sendCreatedDescriptionV2" | i18n: formattedExpirationTime }}
|
||||
</p>
|
||||
|
||||
<bit-form-field class="tw-w-full tw-max-w-sm tw-mb-4">
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
@if (showActionButtons) {
|
||||
<div class="tw-ml-auto">
|
||||
@if ((userCanArchive$ | async) && !params.isAdminConsoleAction) {
|
||||
@if (isCipherArchived) {
|
||||
@if (isCipherArchived && !cipher?.isDeleted) {
|
||||
<button
|
||||
type="button"
|
||||
class="tw-mr-1"
|
||||
|
||||
@@ -5626,13 +5626,13 @@
|
||||
"message": "Send created successfully!",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendCreatedDescription": {
|
||||
"message": "Copy and share this Send link. It can be viewed by the people you specified for the next $TIME$.",
|
||||
"sendCreatedDescriptionV2": {
|
||||
"message": "Copy and share this Send link. The Send will be available to anyone with the link for the next $TIME$.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.",
|
||||
"placeholders": {
|
||||
"time": {
|
||||
"content": "$1",
|
||||
"example": "7 days"
|
||||
"example": "7 days, 1 hour, 1 day"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -40,7 +40,6 @@ export enum FeatureFlag {
|
||||
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
|
||||
EnrollAeadOnKeyRotation = "enroll-aead-on-key-rotation",
|
||||
ForceUpdateKDFSettings = "pm-18021-force-update-kdf-settings",
|
||||
PM25174_DisableType0Decryption = "pm-25174-disable-type-0-decryption",
|
||||
LinuxBiometricsV2 = "pm-26340-linux-biometrics-v2",
|
||||
NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change",
|
||||
DataRecoveryTool = "pm-28813-data-recovery-tool",
|
||||
@@ -150,7 +149,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
|
||||
[FeatureFlag.EnrollAeadOnKeyRotation]: FALSE,
|
||||
[FeatureFlag.ForceUpdateKDFSettings]: FALSE,
|
||||
[FeatureFlag.PM25174_DisableType0Decryption]: FALSE,
|
||||
[FeatureFlag.LinuxBiometricsV2]: FALSE,
|
||||
[FeatureFlag.NoLogoutOnKdfChange]: FALSE,
|
||||
[FeatureFlag.DataRecoveryTool]: FALSE,
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { EncString } from "../models/enc-string";
|
||||
|
||||
export abstract class EncryptService {
|
||||
/**
|
||||
* A temporary init method to make the encrypt service listen to feature-flag changes.
|
||||
* This will be removed once the feature flag has been rolled out.
|
||||
*/
|
||||
abstract init(configService: ConfigService): void;
|
||||
|
||||
/**
|
||||
* Encrypts a string to an EncString
|
||||
* @param plainValue - The value to encrypt
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
@@ -15,28 +13,12 @@ import { PureCrypto } from "@bitwarden/sdk-internal";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
|
||||
export class EncryptServiceImplementation implements EncryptService {
|
||||
private disableType0Decryption = false;
|
||||
|
||||
constructor(
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
protected logService: LogService,
|
||||
protected logMacFailures: boolean,
|
||||
) {}
|
||||
|
||||
init(configService: ConfigService): void {
|
||||
configService.serverConfig$.subscribe((newConfig) => {
|
||||
if (newConfig != null) {
|
||||
this.setDisableType0Decryption(
|
||||
newConfig.featureStates[FeatureFlag.PM25174_DisableType0Decryption] === true,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setDisableType0Decryption(disable: boolean): void {
|
||||
this.disableType0Decryption = disable;
|
||||
}
|
||||
|
||||
async encryptString(plainValue: string, key: SymmetricCryptoKey): Promise<EncString> {
|
||||
if (plainValue == null) {
|
||||
this.logService.warning(
|
||||
@@ -60,7 +42,7 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
}
|
||||
|
||||
async decryptString(encString: EncString, key: SymmetricCryptoKey): Promise<string> {
|
||||
if (this.disableType0Decryption && encString.encryptionType === EncryptionType.AesCbc256_B64) {
|
||||
if (encString.encryptionType === EncryptionType.AesCbc256_B64) {
|
||||
throw new Error("Decryption of AesCbc256_B64 encrypted data is disabled.");
|
||||
}
|
||||
await SdkLoadService.Ready;
|
||||
@@ -68,7 +50,7 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
}
|
||||
|
||||
async decryptBytes(encString: EncString, key: SymmetricCryptoKey): Promise<Uint8Array> {
|
||||
if (this.disableType0Decryption && encString.encryptionType === EncryptionType.AesCbc256_B64) {
|
||||
if (encString.encryptionType === EncryptionType.AesCbc256_B64) {
|
||||
throw new Error("Decryption of AesCbc256_B64 encrypted data is disabled.");
|
||||
}
|
||||
await SdkLoadService.Ready;
|
||||
@@ -76,7 +58,7 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
}
|
||||
|
||||
async decryptFileData(encBuffer: EncArrayBuffer, key: SymmetricCryptoKey): Promise<Uint8Array> {
|
||||
if (this.disableType0Decryption && encBuffer.encryptionType === EncryptionType.AesCbc256_B64) {
|
||||
if (encBuffer.encryptionType === EncryptionType.AesCbc256_B64) {
|
||||
throw new Error("Decryption of AesCbc256_B64 encrypted data is disabled.");
|
||||
}
|
||||
await SdkLoadService.Ready;
|
||||
@@ -148,10 +130,7 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
throw new Error("No wrappingKey provided for unwrapping.");
|
||||
}
|
||||
|
||||
if (
|
||||
this.disableType0Decryption &&
|
||||
wrappedDecapsulationKey.encryptionType === EncryptionType.AesCbc256_B64
|
||||
) {
|
||||
if (wrappedDecapsulationKey.encryptionType === EncryptionType.AesCbc256_B64) {
|
||||
throw new Error("Decryption of AesCbc256_B64 encrypted data is disabled.");
|
||||
}
|
||||
|
||||
@@ -171,10 +150,7 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
if (wrappingKey == null) {
|
||||
throw new Error("No wrappingKey provided for unwrapping.");
|
||||
}
|
||||
if (
|
||||
this.disableType0Decryption &&
|
||||
wrappedEncapsulationKey.encryptionType === EncryptionType.AesCbc256_B64
|
||||
) {
|
||||
if (wrappedEncapsulationKey.encryptionType === EncryptionType.AesCbc256_B64) {
|
||||
throw new Error("Decryption of AesCbc256_B64 encrypted data is disabled.");
|
||||
}
|
||||
|
||||
@@ -194,10 +170,7 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
if (wrappingKey == null) {
|
||||
throw new Error("No wrappingKey provided for unwrapping.");
|
||||
}
|
||||
if (
|
||||
this.disableType0Decryption &&
|
||||
keyToBeUnwrapped.encryptionType === EncryptionType.AesCbc256_B64
|
||||
) {
|
||||
if (keyToBeUnwrapped.encryptionType === EncryptionType.AesCbc256_B64) {
|
||||
throw new Error("Decryption of AesCbc256_B64 encrypted data is disabled.");
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ describe("EncryptService", () => {
|
||||
describe("decryptString", () => {
|
||||
it("is a proxy to PureCrypto", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const encString = new EncString("encrypted_string");
|
||||
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "encrypted_string");
|
||||
const result = await encryptService.decryptString(encString, key);
|
||||
expect(result).toEqual("decrypted_string");
|
||||
expect(PureCrypto.symmetric_decrypt_string).toHaveBeenCalledWith(
|
||||
@@ -172,8 +172,7 @@ describe("EncryptService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("throws if disableType0Decryption is enabled and type is AesCbc256_B64", async () => {
|
||||
encryptService.setDisableType0Decryption(true);
|
||||
it("throws if type is AesCbc256_B64", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "encrypted_string");
|
||||
await expect(encryptService.decryptString(encString, key)).rejects.toThrow(
|
||||
@@ -185,7 +184,7 @@ describe("EncryptService", () => {
|
||||
describe("decryptBytes", () => {
|
||||
it("is a proxy to PureCrypto", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const encString = new EncString("encrypted_bytes");
|
||||
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "encrypted_bytes");
|
||||
const result = await encryptService.decryptBytes(encString, key);
|
||||
expect(result).toEqual(new Uint8Array(3));
|
||||
expect(PureCrypto.symmetric_decrypt_bytes).toHaveBeenCalledWith(
|
||||
@@ -194,8 +193,7 @@ describe("EncryptService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("throws if disableType0Decryption is enabled and type is AesCbc256_B64", async () => {
|
||||
encryptService.setDisableType0Decryption(true);
|
||||
it("throws if type is AesCbc256_B64", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "encrypted_bytes");
|
||||
await expect(encryptService.decryptBytes(encString, key)).rejects.toThrow(
|
||||
@@ -216,8 +214,7 @@ describe("EncryptService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("throws if disableType0Decryption is enabled and type is AesCbc256_B64", async () => {
|
||||
encryptService.setDisableType0Decryption(true);
|
||||
it("throws if type is AesCbc256_B64", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const encBuffer = EncArrayBuffer.fromParts(
|
||||
EncryptionType.AesCbc256_B64,
|
||||
@@ -234,7 +231,10 @@ describe("EncryptService", () => {
|
||||
describe("unwrapDecapsulationKey", () => {
|
||||
it("is a proxy to PureCrypto", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const encString = new EncString("wrapped_decapsulation_key");
|
||||
const encString = new EncString(
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
"wrapped_decapsulation_key",
|
||||
);
|
||||
const result = await encryptService.unwrapDecapsulationKey(encString, key);
|
||||
expect(result).toEqual(new Uint8Array(4));
|
||||
expect(PureCrypto.unwrap_decapsulation_key).toHaveBeenCalledWith(
|
||||
@@ -242,8 +242,7 @@ describe("EncryptService", () => {
|
||||
key.toEncoded(),
|
||||
);
|
||||
});
|
||||
it("throws if disableType0Decryption is enabled and type is AesCbc256_B64", async () => {
|
||||
encryptService.setDisableType0Decryption(true);
|
||||
it("throws if type is AesCbc256_B64", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "wrapped_decapsulation_key");
|
||||
await expect(encryptService.unwrapDecapsulationKey(encString, key)).rejects.toThrow(
|
||||
@@ -267,7 +266,10 @@ describe("EncryptService", () => {
|
||||
describe("unwrapEncapsulationKey", () => {
|
||||
it("is a proxy to PureCrypto", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const encString = new EncString("wrapped_encapsulation_key");
|
||||
const encString = new EncString(
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
"wrapped_encapsulation_key",
|
||||
);
|
||||
const result = await encryptService.unwrapEncapsulationKey(encString, key);
|
||||
expect(result).toEqual(new Uint8Array(5));
|
||||
expect(PureCrypto.unwrap_encapsulation_key).toHaveBeenCalledWith(
|
||||
@@ -275,8 +277,7 @@ describe("EncryptService", () => {
|
||||
key.toEncoded(),
|
||||
);
|
||||
});
|
||||
it("throws if disableType0Decryption is enabled and type is AesCbc256_B64", async () => {
|
||||
encryptService.setDisableType0Decryption(true);
|
||||
it("throws if type is AesCbc256_B64", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "wrapped_encapsulation_key");
|
||||
await expect(encryptService.unwrapEncapsulationKey(encString, key)).rejects.toThrow(
|
||||
@@ -300,7 +301,10 @@ describe("EncryptService", () => {
|
||||
describe("unwrapSymmetricKey", () => {
|
||||
it("is a proxy to PureCrypto", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const encString = new EncString("wrapped_symmetric_key");
|
||||
const encString = new EncString(
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
"wrapped_symmetric_key",
|
||||
);
|
||||
const result = await encryptService.unwrapSymmetricKey(encString, key);
|
||||
expect(result).toEqual(new SymmetricCryptoKey(new Uint8Array(64)));
|
||||
expect(PureCrypto.unwrap_symmetric_key).toHaveBeenCalledWith(
|
||||
@@ -308,8 +312,7 @@ describe("EncryptService", () => {
|
||||
key.toEncoded(),
|
||||
);
|
||||
});
|
||||
it("throws if disableType0Decryption is enabled and type is AesCbc256_B64", async () => {
|
||||
encryptService.setDisableType0Decryption(true);
|
||||
it("throws if type is AesCbc256_B64", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "wrapped_symmetric_key");
|
||||
await expect(encryptService.unwrapSymmetricKey(encString, key)).rejects.toThrow(
|
||||
|
||||
@@ -7,10 +7,10 @@ import { Icon } from "@bitwarden/assets/svg";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { Translation } from "../dialog";
|
||||
import { LandingContentMaxWidthType } from "../landing-layout";
|
||||
|
||||
import { AnonLayoutWrapperDataService } from "./anon-layout-wrapper-data.service";
|
||||
import { AnonLayoutComponent, AnonLayoutMaxWidth } from "./anon-layout.component";
|
||||
|
||||
import { AnonLayoutComponent } from "./anon-layout.component";
|
||||
export interface AnonLayoutWrapperData {
|
||||
/**
|
||||
* The optional title of the page.
|
||||
@@ -35,7 +35,7 @@ export interface AnonLayoutWrapperData {
|
||||
/**
|
||||
* Optional flag to set the max-width of the page. Defaults to 'md' if not provided.
|
||||
*/
|
||||
maxWidth?: AnonLayoutMaxWidth;
|
||||
maxWidth?: LandingContentMaxWidthType;
|
||||
/**
|
||||
* Hide the card that wraps the default content. Defaults to false.
|
||||
*/
|
||||
@@ -59,7 +59,7 @@ export class AnonLayoutWrapperComponent implements OnInit {
|
||||
protected pageSubtitle?: string | null;
|
||||
protected pageIcon: Icon | null = null;
|
||||
protected showReadonlyHostname?: boolean | null;
|
||||
protected maxWidth?: AnonLayoutMaxWidth | null;
|
||||
protected maxWidth?: LandingContentMaxWidthType | null;
|
||||
protected hideCardWrapper?: boolean | null;
|
||||
protected hideBackgroundIllustration?: boolean | null;
|
||||
|
||||
|
||||
@@ -1,76 +1,26 @@
|
||||
<main
|
||||
class="tw-relative tw-flex tw-w-full tw-mx-auto tw-flex-col tw-bg-background-alt tw-p-5 tw-text-main"
|
||||
[ngClass]="{
|
||||
'tw-min-h-screen': clientType === 'web',
|
||||
'tw-min-h-full': clientType === 'browser' || clientType === 'desktop',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
[class]="
|
||||
'tw-flex tw-justify-between tw-items-center tw-w-full' + (!hideLogo() ? ' tw-mb-12' : '')
|
||||
"
|
||||
>
|
||||
@if (!hideLogo()) {
|
||||
<a
|
||||
[routerLink]="['/']"
|
||||
class="tw-w-32 sm:tw-w-[200px] tw-self-center sm:tw-self-start tw-block [&>*]:tw-align-top"
|
||||
>
|
||||
<bit-icon [icon]="logo" [ariaLabel]="'appLogoLabel' | i18n"></bit-icon>
|
||||
</a>
|
||||
}
|
||||
<div class="tw-ms-auto">
|
||||
<ng-content select="[slot=header-actions]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
<bit-landing-layout [hideBackgroundIllustration]="hideBackgroundIllustration()">
|
||||
<bit-landing-header [hideLogo]="hideLogo()">
|
||||
<ng-content select="[slot=header-actions]"></ng-content>
|
||||
</bit-landing-header>
|
||||
|
||||
<div class="tw-text-center tw-mb-4 sm:tw-mb-6 tw-mx-auto" [ngClass]="maxWidthClass">
|
||||
@let iconInput = icon();
|
||||
|
||||
<!-- In some scenarios this icon's size is not limited by container width correctly -->
|
||||
<!-- Targeting the SVG here to try and ensure it never grows too large in even the media queries are not working as expected -->
|
||||
<div
|
||||
*ngIf="iconInput !== null"
|
||||
class="tw-size-20 sm:tw-size-24 [&_svg]:tw-w-full [&_svg]:tw-max-w-24 tw-mx-auto tw-content-center"
|
||||
>
|
||||
<bit-icon [icon]="iconInput"></bit-icon>
|
||||
</div>
|
||||
|
||||
@if (title()) {
|
||||
<!-- Small screens -->
|
||||
<h1 bitTypography="h2" class="tw-mt-2 sm:tw-hidden">
|
||||
{{ title() }}
|
||||
</h1>
|
||||
<!-- Medium to Larger screens -->
|
||||
<h1 bitTypography="h1" class="tw-mt-2 tw-hidden sm:tw-block">
|
||||
{{ title() }}
|
||||
</h1>
|
||||
}
|
||||
|
||||
@if (subtitle()) {
|
||||
<div class="tw-text-sm sm:tw-text-base">{{ subtitle() }}</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="tw-z-10 tw-grow tw-w-full tw-mx-auto tw-flex tw-flex-col tw-items-center sm:tw-min-w-[28rem]"
|
||||
[ngClass]="maxWidthClass"
|
||||
>
|
||||
<bit-landing-content [maxWidth]="maxWidth()">
|
||||
<bit-landing-hero [icon]="icon()" [title]="title()" [subtitle]="subtitle()"></bit-landing-hero>
|
||||
@if (hideCardWrapper()) {
|
||||
<div class="tw-mb-6 sm:tw-mb-10">
|
||||
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
|
||||
</div>
|
||||
} @else {
|
||||
<bit-base-card
|
||||
class="!tw-rounded-2xl tw-mb-6 sm:tw-mb-10 tw-mx-auto tw-w-full tw-bg-transparent tw-border-none tw-shadow-none sm:tw-bg-background sm:tw-border sm:tw-border-solid sm:tw-border-secondary-100 sm:tw-shadow sm:tw-p-8"
|
||||
>
|
||||
<bit-landing-card>
|
||||
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
|
||||
</bit-base-card>
|
||||
</bit-landing-card>
|
||||
}
|
||||
<ng-content select="[slot=secondary]"></ng-content>
|
||||
</div>
|
||||
<div class="tw-flex tw-flex-col tw-items-center">
|
||||
<ng-content select="[slot=secondary]"></ng-content>
|
||||
</div>
|
||||
</bit-landing-content>
|
||||
|
||||
@if (!hideFooter()) {
|
||||
<footer class="tw-text-center tw-mt-4 sm:tw-mt-6">
|
||||
<bit-landing-footer>
|
||||
@if (showReadonlyHostname()) {
|
||||
<div bitTypography="body2">{{ "accessing" | i18n }} {{ hostname }}</div>
|
||||
} @else {
|
||||
@@ -81,22 +31,9 @@
|
||||
<div bitTypography="body2">© {{ year }} Bitwarden Inc.</div>
|
||||
<div bitTypography="body2">{{ version }}</div>
|
||||
}
|
||||
</footer>
|
||||
</bit-landing-footer>
|
||||
}
|
||||
|
||||
@if (!hideBackgroundIllustration()) {
|
||||
<div
|
||||
class="tw-hidden md:tw-block [&_svg]:tw-absolute tw-z-[1] tw-opacity-[.11] [&_svg]:tw-bottom-0 [&_svg]:tw-start-0 [&_svg]:tw-w-[35%] [&_svg]:tw-max-w-[450px]"
|
||||
>
|
||||
<bit-icon [icon]="leftIllustration"></bit-icon>
|
||||
</div>
|
||||
<div
|
||||
class="tw-hidden md:tw-block [&_svg]:tw-absolute tw-z-[1] tw-opacity-[.11] [&_svg]:tw-bottom-0 [&_svg]:tw-end-0 [&_svg]:tw-w-[35%] [&_svg]:tw-max-w-[450px]"
|
||||
>
|
||||
<bit-icon [icon]="rightIllustration"></bit-icon>
|
||||
</div>
|
||||
}
|
||||
</main>
|
||||
</bit-landing-layout>
|
||||
|
||||
<ng-template #defaultContent>
|
||||
<ng-content></ng-content>
|
||||
|
||||
@@ -11,23 +11,17 @@ import {
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import {
|
||||
BackgroundLeftIllustration,
|
||||
BackgroundRightIllustration,
|
||||
BitwardenLogo,
|
||||
Icon,
|
||||
} from "@bitwarden/assets/svg";
|
||||
import { BitwardenLogo, Icon } from "@bitwarden/assets/svg";
|
||||
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 { BaseCardComponent } from "../card";
|
||||
import { IconModule } from "../icon";
|
||||
import { LandingContentMaxWidthType } from "../landing-layout";
|
||||
import { LandingLayoutModule } from "../landing-layout/landing-layout.module";
|
||||
import { SharedModule } from "../shared";
|
||||
import { TypographyModule } from "../typography";
|
||||
|
||||
export type AnonLayoutMaxWidth = "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
@@ -39,7 +33,7 @@ export type AnonLayoutMaxWidth = "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
|
||||
TypographyModule,
|
||||
SharedModule,
|
||||
RouterModule,
|
||||
BaseCardComponent,
|
||||
LandingLayoutModule,
|
||||
],
|
||||
})
|
||||
export class AnonLayoutComponent implements OnInit, OnChanges {
|
||||
@@ -49,9 +43,6 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
|
||||
return ["tw-h-full"];
|
||||
}
|
||||
|
||||
readonly leftIllustration = BackgroundLeftIllustration;
|
||||
readonly rightIllustration = BackgroundRightIllustration;
|
||||
|
||||
readonly title = input<string>();
|
||||
readonly subtitle = input<string>();
|
||||
readonly icon = model.required<Icon | null>();
|
||||
@@ -66,7 +57,7 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
|
||||
*
|
||||
* @default 'md'
|
||||
*/
|
||||
readonly maxWidth = model<AnonLayoutMaxWidth>("md");
|
||||
readonly maxWidth = model<LandingContentMaxWidthType>("md");
|
||||
|
||||
protected logo = BitwardenLogo;
|
||||
protected year: string;
|
||||
@@ -76,24 +67,6 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
|
||||
|
||||
protected hideYearAndVersion = false;
|
||||
|
||||
get maxWidthClass(): string {
|
||||
const maxWidth = this.maxWidth();
|
||||
switch (maxWidth) {
|
||||
case "md":
|
||||
return "tw-max-w-md";
|
||||
case "lg":
|
||||
return "tw-max-w-lg";
|
||||
case "xl":
|
||||
return "tw-max-w-xl";
|
||||
case "2xl":
|
||||
return "tw-max-w-2xl";
|
||||
case "3xl":
|
||||
return "tw-max-w-3xl";
|
||||
case "4xl":
|
||||
return "tw-max-w-4xl";
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private environmentService: EnvironmentService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
|
||||
@@ -25,6 +25,7 @@ export * from "./icon";
|
||||
export * from "./icon-tile";
|
||||
export * from "./input";
|
||||
export * from "./item";
|
||||
export * from "./landing-layout";
|
||||
export * from "./layout";
|
||||
export * from "./link";
|
||||
export * from "./menu";
|
||||
|
||||
7
libs/components/src/landing-layout/index.ts
Normal file
7
libs/components/src/landing-layout/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from "./landing-layout.component";
|
||||
export * from "./landing-layout.module";
|
||||
export * from "./landing-card.component";
|
||||
export * from "./landing-content.component";
|
||||
export * from "./landing-footer.component";
|
||||
export * from "./landing-header.component";
|
||||
export * from "./landing-hero.component";
|
||||
@@ -0,0 +1,5 @@
|
||||
<bit-base-card
|
||||
class="tw-z-[2] tw-relative !tw-rounded-2xl tw-mb-6 sm:tw-mb-10 tw-mx-auto tw-w-full tw-bg-transparent tw-border-none tw-shadow-none sm:tw-bg-background sm:tw-border sm:tw-border-solid sm:tw-border-secondary-100 sm:tw-shadow sm:tw-p-8"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
</bit-base-card>
|
||||
33
libs/components/src/landing-layout/landing-card.component.ts
Normal file
33
libs/components/src/landing-layout/landing-card.component.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
|
||||
import { BaseCardComponent } from "../card";
|
||||
|
||||
/**
|
||||
* Card component for landing pages that wraps content in a styled container.
|
||||
*
|
||||
* @remarks
|
||||
* This component provides:
|
||||
* - Card-based layout with consistent styling
|
||||
* - Content projection for forms, text, or other content
|
||||
* - Proper elevation and border styling
|
||||
*
|
||||
* Use this component inside `bit-landing-content` to wrap forms, content sections,
|
||||
* or any content that should appear in a contained, elevated card.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <bit-landing-card>
|
||||
* <form>
|
||||
* <!-- Your form fields here -->
|
||||
* </form>
|
||||
* </bit-landing-card>
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: "bit-landing-card",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [BaseCardComponent],
|
||||
templateUrl: "./landing-card.component.html",
|
||||
})
|
||||
export class LandingCardComponent {}
|
||||
@@ -0,0 +1,8 @@
|
||||
<div
|
||||
class="tw-flex tw-flex-col tw-flex-1 tw-items-center tw-bg-background-alt tw-p-5 tw-pt-12 tw-text-main"
|
||||
>
|
||||
<div [class]="maxWidthClasses()">
|
||||
<ng-content select="bit-landing-hero"></ng-content>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,63 @@
|
||||
import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core";
|
||||
|
||||
export const LandingContentMaxWidth = ["md", "lg", "xl", "2xl", "3xl", "4xl"] as const;
|
||||
|
||||
export type LandingContentMaxWidthType = (typeof LandingContentMaxWidth)[number];
|
||||
|
||||
/**
|
||||
* Main content container for landing pages with configurable max-width constraints.
|
||||
*
|
||||
* @remarks
|
||||
* This component provides:
|
||||
* - Centered content area with alternative background color
|
||||
* - Configurable maximum width to control content readability
|
||||
* - Content projection slots for hero section and main content
|
||||
* - Responsive padding and layout
|
||||
*
|
||||
* Use this component inside `bit-landing-layout` to wrap your main page content.
|
||||
* Optionally include a `bit-landing-hero` as the first child for consistent hero section styling.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <bit-landing-content [maxWidth]="'xl'">
|
||||
* <bit-landing-hero
|
||||
* [icon]="lockIcon"
|
||||
* [title]="'Welcome'"
|
||||
* [subtitle]="'Get started with your account'"
|
||||
* ></bit-landing-hero>
|
||||
* <bit-landing-card>
|
||||
* <!-- Your form or content here -->
|
||||
* </bit-landing-card>
|
||||
* </bit-landing-content>
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: "bit-landing-content",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: "./landing-content.component.html",
|
||||
host: {
|
||||
class: "tw-grow tw-flex tw-flex-col",
|
||||
},
|
||||
})
|
||||
export class LandingContentComponent {
|
||||
/**
|
||||
* Max width of the landing layout container.
|
||||
*
|
||||
* @default "md"
|
||||
*/
|
||||
readonly maxWidth = input<LandingContentMaxWidthType>("md");
|
||||
|
||||
private readonly maxWidthClassMap: Record<LandingContentMaxWidthType, string> = {
|
||||
md: "tw-max-w-md",
|
||||
lg: "tw-max-w-lg",
|
||||
xl: "tw-max-w-xl",
|
||||
"2xl": "tw-max-w-2xl",
|
||||
"3xl": "tw-max-w-3xl",
|
||||
"4xl": "tw-max-w-4xl",
|
||||
};
|
||||
|
||||
readonly maxWidthClasses = computed(() => {
|
||||
const maxWidthClass = this.maxWidthClassMap[this.maxWidth()];
|
||||
return `tw-flex tw-flex-col tw-w-full ${maxWidthClass}`;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<footer class="tw-bg-background-alt tw-text-center tw-p-5 tw-pt-4 sm:tw-pt-6">
|
||||
<ng-content></ng-content>
|
||||
</footer>
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
|
||||
/**
|
||||
* Footer component for landing pages.
|
||||
*
|
||||
* @remarks
|
||||
* This component provides:
|
||||
* - Content projection for custom footer content (e.g., links, copyright, legal)
|
||||
* - Consistent footer positioning at the bottom of the page
|
||||
* - Proper z-index to appear above background illustrations
|
||||
*
|
||||
* Use this component inside `bit-landing-layout` as the last child to position it at the bottom.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <bit-landing-footer>
|
||||
* <div class="tw-text-center tw-text-sm">
|
||||
* <a routerLink="/privacy">Privacy</a>
|
||||
* <span>© 2024 Bitwarden</span>
|
||||
* </div>
|
||||
* </bit-landing-footer>
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: "bit-landing-footer",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: "./landing-footer.component.html",
|
||||
})
|
||||
export class LandingFooterComponent {}
|
||||
@@ -0,0 +1,13 @@
|
||||
<header class="tw-flex tw-w-full tw-bg-background-alt tw-px-5">
|
||||
@if (!hideLogo()) {
|
||||
<a
|
||||
[routerLink]="['/']"
|
||||
class="tw-w-32 tw-py-5 sm:tw-w-[200px] tw-self-center sm:tw-self-start tw-block [&>*]:tw-align-top"
|
||||
>
|
||||
<bit-icon [icon]="logo" [ariaLabel]="'appLogoLabel' | i18n"></bit-icon>
|
||||
</a>
|
||||
}
|
||||
<div class="[&:has(*)]:tw-ms-auto [&:has(*)]:tw-py-5">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</header>
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
|
||||
import { BitwardenLogo } from "@bitwarden/assets/svg";
|
||||
|
||||
import { IconModule } from "../icon";
|
||||
import { SharedModule } from "../shared";
|
||||
|
||||
/**
|
||||
* Header component for landing pages with optional Bitwarden logo and header actions slot.
|
||||
*
|
||||
* @remarks
|
||||
* This component provides:
|
||||
* - Optional Bitwarden logo with link to home page (left-aligned)
|
||||
* - Default content projection slot for header actions (right-aligned, auto-margin left)
|
||||
* - Consistent header styling across landing pages
|
||||
* - Responsive layout that adapts logo size
|
||||
*
|
||||
* Use this component inside `bit-landing-layout` as the first child to position it at the top.
|
||||
* Content projected into this component will automatically align to the right side of the header.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <bit-landing-header [hideLogo]="false">
|
||||
* <!-- Content here appears in the right-aligned actions slot -->
|
||||
* <nav>
|
||||
* <a routerLink="/login">Log in</a>
|
||||
* <button type="button">Sign up</button>
|
||||
* </nav>
|
||||
* </bit-landing-header>
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: "bit-landing-header",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: "./landing-header.component.html",
|
||||
imports: [RouterModule, IconModule, SharedModule],
|
||||
})
|
||||
export class LandingHeaderComponent {
|
||||
readonly hideLogo = input<boolean>(false);
|
||||
protected readonly logo = BitwardenLogo;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
@if (icon() || title() || subtitle()) {
|
||||
<div class="tw-text-center tw-mb-4 sm:tw-mb-6 tw-mx-auto">
|
||||
@if (icon()) {
|
||||
<!-- In some scenarios this icon's size is not limited by container width correctly -->
|
||||
<!-- Targeting the SVG here to try and ensure it never grows too large in even the media queries are not working as expected -->
|
||||
<div
|
||||
class="tw-size-20 sm:tw-size-24 [&_svg]:tw-w-full [&_svg]:tw-max-w-24 tw-mx-auto tw-content-center"
|
||||
>
|
||||
<bit-icon [icon]="icon()"></bit-icon>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (title()) {
|
||||
<!-- Small screens -->
|
||||
<h1 bitTypography="h2" class="tw-mt-2 sm:tw-hidden">
|
||||
{{ title() }}
|
||||
</h1>
|
||||
<!-- Medium to Larger screens -->
|
||||
<h1 bitTypography="h1" class="tw-mt-2 tw-hidden sm:tw-block">
|
||||
{{ title() }}
|
||||
</h1>
|
||||
}
|
||||
|
||||
@if (subtitle()) {
|
||||
<div class="tw-text-sm sm:tw-text-base">{{ subtitle() }}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
40
libs/components/src/landing-layout/landing-hero.component.ts
Normal file
40
libs/components/src/landing-layout/landing-hero.component.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from "@angular/core";
|
||||
|
||||
import { Icon } from "@bitwarden/assets/svg";
|
||||
|
||||
import { IconModule } from "../icon";
|
||||
import { TypographyModule } from "../typography";
|
||||
|
||||
/**
|
||||
* Hero section component for landing pages featuring an optional icon, title, and subtitle.
|
||||
*
|
||||
* @remarks
|
||||
* This component provides:
|
||||
* - Optional icon display (e.g., feature icons, status icons)
|
||||
* - Large title text with consistent typography
|
||||
* - Subtitle text for additional context
|
||||
* - Centered layout with proper spacing
|
||||
*
|
||||
* Use this component as the first child inside `bit-landing-content` to create a prominent
|
||||
* hero section that introduces the page's purpose.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <bit-landing-hero
|
||||
* [icon]="lockIcon"
|
||||
* [title]="'Secure Your Passwords'"
|
||||
* [subtitle]="'Create your account to get started'"
|
||||
* ></bit-landing-hero>
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: "bit-landing-hero",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: "./landing-hero.component.html",
|
||||
imports: [IconModule, TypographyModule],
|
||||
})
|
||||
export class LandingHeroComponent {
|
||||
readonly icon = input<Icon | null>(null);
|
||||
readonly title = input<string | undefined>();
|
||||
readonly subtitle = input<string | undefined>();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<div
|
||||
class="tw-relative tw-flex tw-size-full tw-mx-auto tw-flex-col"
|
||||
[class]="{
|
||||
'tw-min-h-screen': clientType === 'web',
|
||||
'tw-min-h-full': clientType === 'browser' || clientType === 'desktop',
|
||||
}"
|
||||
>
|
||||
<ng-content select="bit-landing-header"></ng-content>
|
||||
<main class="tw-relative tw-flex tw-flex-1 tw-size-full tw-mx-auto tw-flex-col">
|
||||
<ng-content></ng-content>
|
||||
</main>
|
||||
@if (!hideBackgroundIllustration()) {
|
||||
<div
|
||||
class="tw-hidden md:tw-block [&_svg]:tw-absolute tw-z-[1] tw-opacity-[.11] [&_svg]:tw-bottom-0 [&_svg]:tw-start-0 [&_svg]:tw-w-[35%] [&_svg]:tw-max-w-[450px]"
|
||||
>
|
||||
<bit-icon [icon]="leftIllustration"></bit-icon>
|
||||
</div>
|
||||
<div
|
||||
class="tw-hidden md:tw-block [&_svg]:tw-absolute tw-z-[1] tw-opacity-[.11] [&_svg]:tw-bottom-0 [&_svg]:tw-end-0 [&_svg]:tw-w-[35%] [&_svg]:tw-max-w-[450px]"
|
||||
>
|
||||
<bit-icon [icon]="rightIllustration"></bit-icon>
|
||||
</div>
|
||||
}
|
||||
<ng-content select="bit-landing-footer"></ng-content>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Component, ChangeDetectionStrategy, inject, input } from "@angular/core";
|
||||
|
||||
import { BackgroundLeftIllustration, BackgroundRightIllustration } from "@bitwarden/assets/svg";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { IconModule } from "../icon";
|
||||
|
||||
/**
|
||||
* Root layout component for landing pages providing a full-screen container with optional decorative background illustrations.
|
||||
*
|
||||
* @remarks
|
||||
* This component serves as the outermost wrapper for landing pages and provides:
|
||||
* - Full-screen layout that adapts to different client types (web, browser, desktop)
|
||||
* - Optional decorative background illustrations in the bottom corners
|
||||
* - Content projection slots for header, main content, and footer
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <bit-landing-layout [hideBackgroundIllustration]="false">
|
||||
* <bit-landing-header>...</bit-landing-header>
|
||||
* <bit-landing-content>...</bit-landing-content>
|
||||
* <bit-landing-footer>...</bit-landing-footer>
|
||||
* </bit-landing-layout>
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: "bit-landing-layout",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: "./landing-layout.component.html",
|
||||
imports: [IconModule],
|
||||
})
|
||||
export class LandingLayoutComponent {
|
||||
readonly hideBackgroundIllustration = input<boolean>(false);
|
||||
|
||||
protected readonly leftIllustration = BackgroundLeftIllustration;
|
||||
protected readonly rightIllustration = BackgroundRightIllustration;
|
||||
|
||||
private readonly platformUtilsService: PlatformUtilsService = inject(PlatformUtilsService);
|
||||
protected readonly clientType = this.platformUtilsService.getClientType();
|
||||
}
|
||||
28
libs/components/src/landing-layout/landing-layout.module.ts
Normal file
28
libs/components/src/landing-layout/landing-layout.module.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { LandingCardComponent } from "./landing-card.component";
|
||||
import { LandingContentComponent } from "./landing-content.component";
|
||||
import { LandingFooterComponent } from "./landing-footer.component";
|
||||
import { LandingHeaderComponent } from "./landing-header.component";
|
||||
import { LandingHeroComponent } from "./landing-hero.component";
|
||||
import { LandingLayoutComponent } from "./landing-layout.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
LandingLayoutComponent,
|
||||
LandingHeaderComponent,
|
||||
LandingHeroComponent,
|
||||
LandingFooterComponent,
|
||||
LandingContentComponent,
|
||||
LandingCardComponent,
|
||||
],
|
||||
exports: [
|
||||
LandingLayoutComponent,
|
||||
LandingHeaderComponent,
|
||||
LandingHeroComponent,
|
||||
LandingFooterComponent,
|
||||
LandingContentComponent,
|
||||
LandingCardComponent,
|
||||
],
|
||||
})
|
||||
export class LandingLayoutModule {}
|
||||
162
libs/components/src/landing-layout/landing-layout.stories.ts
Normal file
162
libs/components/src/landing-layout/landing-layout.stories.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { ButtonModule } from "../button";
|
||||
|
||||
import { LandingLayoutComponent } from "./landing-layout.component";
|
||||
|
||||
class MockPlatformUtilsService implements Partial<PlatformUtilsService> {
|
||||
getClientType = () => ClientType.Web;
|
||||
}
|
||||
|
||||
type StoryArgs = LandingLayoutComponent & {
|
||||
contentLength: "normal" | "long" | "thin";
|
||||
includeHeader: boolean;
|
||||
includeFooter: boolean;
|
||||
};
|
||||
|
||||
export default {
|
||||
title: "Component Library/Landing Layout",
|
||||
component: LandingLayoutComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [ButtonModule],
|
||||
providers: [
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useClass: MockPlatformUtilsService,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
render: (args) => {
|
||||
return {
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<bit-landing-layout
|
||||
[hideBackgroundIllustration]="hideBackgroundIllustration"
|
||||
>
|
||||
@if (includeHeader) {
|
||||
<bit-landing-header>
|
||||
<div class="tw-p-4">
|
||||
<div class="tw-flex tw-items-center tw-gap-4">
|
||||
<div class="tw-text-xl tw-font-semibold">Header Content</div>
|
||||
</div>
|
||||
</div>
|
||||
</bit-landing-header>
|
||||
}
|
||||
|
||||
<div>
|
||||
@switch (contentLength) {
|
||||
@case ('thin') {
|
||||
<div class="tw-text-center tw-p-8">
|
||||
<div class="tw-font-medium">Thin Content</div>
|
||||
</div>
|
||||
}
|
||||
@case ('long') {
|
||||
<div class="tw-p-8">
|
||||
<div class="tw-font-medium tw-mb-4">Long Content</div>
|
||||
<div class="tw-mb-4">Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?</div>
|
||||
<div class="tw-mb-4">Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?</div>
|
||||
</div>
|
||||
}
|
||||
@default {
|
||||
<div class="tw-p-8">
|
||||
<div class="tw-font-medium tw-mb-4">Normal Content</div>
|
||||
<div>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (includeFooter) {
|
||||
<bit-landing-footer>
|
||||
<div class="tw-text-center tw-text-sm tw-text-muted">
|
||||
<div>Footer Content</div>
|
||||
</div>
|
||||
</bit-landing-footer>
|
||||
}
|
||||
</bit-landing-layout>
|
||||
`,
|
||||
};
|
||||
},
|
||||
|
||||
argTypes: {
|
||||
hideBackgroundIllustration: { control: "boolean" },
|
||||
contentLength: {
|
||||
control: "radio",
|
||||
options: ["normal", "long", "thin"],
|
||||
},
|
||||
includeHeader: { control: "boolean" },
|
||||
includeFooter: { control: "boolean" },
|
||||
},
|
||||
|
||||
args: {
|
||||
hideBackgroundIllustration: false,
|
||||
contentLength: "normal",
|
||||
includeHeader: false,
|
||||
includeFooter: false,
|
||||
},
|
||||
} satisfies Meta<StoryArgs>;
|
||||
|
||||
type Story = StoryObj<StoryArgs>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
contentLength: "normal",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithHeader: Story = {
|
||||
args: {
|
||||
includeHeader: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithFooter: Story = {
|
||||
args: {
|
||||
includeFooter: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithHeaderAndFooter: Story = {
|
||||
args: {
|
||||
includeHeader: true,
|
||||
includeFooter: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const LongContent: Story = {
|
||||
args: {
|
||||
contentLength: "long",
|
||||
includeHeader: true,
|
||||
includeFooter: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const ThinContent: Story = {
|
||||
args: {
|
||||
contentLength: "thin",
|
||||
includeHeader: true,
|
||||
includeFooter: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const NoBackgroundIllustration: Story = {
|
||||
args: {
|
||||
hideBackgroundIllustration: true,
|
||||
includeHeader: true,
|
||||
includeFooter: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const MinimalState: Story = {
|
||||
args: {
|
||||
contentLength: "thin",
|
||||
hideBackgroundIllustration: true,
|
||||
includeHeader: false,
|
||||
includeFooter: false,
|
||||
},
|
||||
};
|
||||
@@ -8,7 +8,7 @@
|
||||
id="newItemDropdown"
|
||||
[appA11yTitle]="'new' | i18n"
|
||||
>
|
||||
<i class="bwi bwi-plus" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-plus tw-me-2" aria-hidden="true"></i>
|
||||
{{ "new" | i18n }}
|
||||
</button>
|
||||
<bit-menu #addOptions aria-labelledby="newItemDropdown">
|
||||
|
||||
Reference in New Issue
Block a user