1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-01 09:13:54 +00:00

Merge main

This commit is contained in:
Bernd Schoolmann
2025-12-12 11:31:50 +01:00
964 changed files with 37979 additions and 16929 deletions

View File

@@ -3,9 +3,7 @@ import { DeviceManagementComponentServiceAbstraction } from "./device-management
/**
* Default implementation of the device management component service
*/
export class DefaultDeviceManagementComponentService
implements DeviceManagementComponentServiceAbstraction
{
export class DefaultDeviceManagementComponentService implements DeviceManagementComponentServiceAbstraction {
/**
* Show header information in web client
*/

View File

@@ -3,9 +3,7 @@ import { LoginApprovalDialogComponentServiceAbstraction } from "./login-approval
/**
* Default implementation of the LoginApprovalDialogComponentServiceAbstraction.
*/
export class DefaultLoginApprovalDialogComponentService
implements LoginApprovalDialogComponentServiceAbstraction
{
export class DefaultLoginApprovalDialogComponentService implements LoginApprovalDialogComponentServiceAbstraction {
/**
* No-op implementation of the showLoginRequestedAlertIfWindowNotVisible method.
* @returns

View File

@@ -2,7 +2,7 @@
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { Router, RouterModule } from "@angular/router";
import { ActivatedRoute, Router, RouterModule } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@@ -19,6 +19,7 @@ import { ClientType } from "@bitwarden/common/enums";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import {
@@ -49,6 +50,7 @@ export type State = "assert" | "assertFailed";
})
export class LoginViaWebAuthnComponent implements OnInit {
protected currentState: State = "assert";
private shouldAutoClosePopout = false;
protected readonly Icons = {
TwoFactorAuthSecurityKeyIcon,
@@ -70,6 +72,7 @@ export class LoginViaWebAuthnComponent implements OnInit {
constructor(
private webAuthnLoginService: WebAuthnLoginServiceAbstraction,
private router: Router,
private route: ActivatedRoute,
private logService: LogService,
private validationService: ValidationService,
private i18nService: I18nService,
@@ -77,9 +80,14 @@ export class LoginViaWebAuthnComponent implements OnInit {
private keyService: KeyService,
private platformUtilsService: PlatformUtilsService,
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
private messagingService: MessagingService,
) {}
ngOnInit(): void {
// Check if we should auto-close the popout after successful authentication
this.shouldAutoClosePopout =
this.route.snapshot.queryParamMap.get("autoClosePopout") === "true";
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.authenticate();
@@ -121,6 +129,20 @@ export class LoginViaWebAuthnComponent implements OnInit {
const userKey = await firstValueFrom(this.keyService.userKey$(authResult.userId));
if (userKey) {
await this.loginSuccessHandlerService.run(authResult.userId, null);
<<<<<<< HEAD
=======
}
// If autoClosePopout is enabled and we're in a browser extension,
// re-open the regular popup and close this popout window
if (
this.shouldAutoClosePopout &&
this.platformUtilsService.getClientType() === ClientType.Browser
) {
this.messagingService.send("openPopup");
window.close();
return;
>>>>>>> main
}
await this.router.navigate([this.successRoute]);

View File

@@ -14,10 +14,11 @@ import { BadgeModule } from "@bitwarden/components";
type="button"
*appNotPremium
bitBadge
variant="success"
[variant]="'primary'"
class="!tw-text-primary-600 !tw-border-primary-600"
(click)="promptForPremium($event)"
>
{{ "premium" | i18n }}
<i class="bwi bwi-premium tw-pe-1"></i>{{ "upgrade" | i18n }}
</button>
`,
imports: [BadgeModule, JslibModule],

View File

@@ -29,7 +29,7 @@ export default {
provide: I18nService,
useFactory: () => {
return new I18nMockService({
premium: "Premium",
upgrade: "Upgrade",
});
},
},

View File

@@ -20,33 +20,35 @@
<div
class="tw-box-border tw-bg-background tw-text-main tw-size-full tw-flex tw-flex-col tw-px-8 tw-pb-2 tw-w-full tw-max-w-md"
>
<div class="tw-flex tw-items-center tw-justify-between tw-mb-2">
<div class="tw-flex tw-items-center tw-justify-between">
<h3 slot="title" class="tw-m-0" bitTypography="h3">
{{ "upgradeToPremium" | i18n }}
</h3>
</div>
<!-- Tagline with consistent height (exactly 2 lines) -->
<div class="tw-mb-6 tw-h-6">
<div class="tw-h-6">
<p bitTypography="helper" class="tw-text-muted tw-m-0 tw-leading-relaxed tw-line-clamp-2">
{{ cardDetails.tagline }}
</p>
</div>
<!-- Price Section -->
<div class="tw-mb-6">
<div class="tw-flex tw-items-baseline tw-gap-1 tw-flex-wrap">
<span class="tw-text-3xl tw-font-medium tw-leading-none tw-m-0">{{
cardDetails.price.amount | currency: "$"
}}</span>
<span bitTypography="helper" class="tw-text-muted">
/ {{ cardDetails.price.cadence }}
</span>
@if (cardDetails.price) {
<div class="tw-mt-5">
<div class="tw-flex tw-items-baseline tw-gap-1 tw-flex-wrap">
<span class="tw-text-3xl tw-font-medium tw-leading-none tw-m-0">{{
cardDetails.price.amount | currency: "$"
}}</span>
<span bitTypography="helper" class="tw-text-muted">
/ {{ cardDetails.price.cadence | i18n }}
</span>
</div>
</div>
</div>
}
<!-- Button space (always reserved) -->
<div class="tw-mb-6 tw-h-12">
<div class="tw-my-5 tw-h-12">
<button
bitButton
[buttonType]="cardDetails.button.type"

View File

@@ -40,6 +40,7 @@ describe("PremiumUpgradeDialogComponent", () => {
type: "standalone",
annualPrice: 10,
annualPricePerAdditionalStorageGB: 4,
providedStorageGB: 1,
features: [
{ key: "feature1", value: "Feature 1" },
{ key: "feature2", value: "Feature 2" },
@@ -58,6 +59,7 @@ describe("PremiumUpgradeDialogComponent", () => {
users: 6,
annualPrice: 40,
annualPricePerAdditionalStorageGB: 4,
providedStorageGB: 1,
features: [{ key: "featureA", value: "Feature A" }],
},
};
@@ -204,4 +206,39 @@ describe("PremiumUpgradeDialogComponent", () => {
});
});
});
describe("self-hosted environment", () => {
it("should handle null price data for self-hosted environment", async () => {
const selfHostedPremiumTier: PersonalSubscriptionPricingTier = {
id: PersonalSubscriptionPricingTierIds.Premium,
name: "Premium",
description: "Advanced features for power users",
availableCadences: [SubscriptionCadenceIds.Annually],
passwordManager: {
type: "standalone",
annualPrice: undefined as any, // self-host will have these prices empty
annualPricePerAdditionalStorageGB: undefined as any,
providedStorageGB: undefined as any,
features: [
{ key: "feature1", value: "Feature 1" },
{ key: "feature2", value: "Feature 2" },
],
},
};
mockSubscriptionPricingService.getPersonalSubscriptionPricingTiers$.mockReturnValue(
of([selfHostedPremiumTier]),
);
const selfHostedFixture = TestBed.createComponent(PremiumUpgradeDialogComponent);
const selfHostedComponent = selfHostedFixture.componentInstance;
selfHostedFixture.detectChanges();
const cardDetails = await firstValueFrom(selfHostedComponent["cardDetails$"]);
expect(cardDetails?.title).toBe("Premium");
expect(cardDetails?.price).toBeUndefined();
expect(cardDetails?.features).toEqual(["Feature 1", "Feature 2"]);
});
});
});

View File

@@ -31,6 +31,24 @@ const mockPremiumTier: PersonalSubscriptionPricingTier = {
type: "standalone",
annualPrice: 10,
annualPricePerAdditionalStorageGB: 4,
providedStorageGB: 1,
features: [
{ key: "builtInAuthenticator", value: "Built-in authenticator" },
{ key: "secureFileStorage", value: "Secure file storage" },
{ key: "emergencyAccess", value: "Emergency access" },
{ key: "breachMonitoring", value: "Breach monitoring" },
{ key: "andMoreFeatures", value: "And more!" },
],
},
};
const mockPremiumTierNoPricingData: PersonalSubscriptionPricingTier = {
id: PersonalSubscriptionPricingTierIds.Premium,
name: "Premium",
description: "Complete online security",
availableCadences: [SubscriptionCadenceIds.Annually],
passwordManager: {
type: "standalone",
features: [
{ key: "builtInAuthenticator", value: "Built-in authenticator" },
{ key: "secureFileStorage", value: "Secure file storage" },
@@ -85,11 +103,11 @@ export default {
t: (key: string) => {
switch (key) {
case "upgradeNow":
return "Upgrade Now";
return "Upgrade now";
case "month":
return "month";
case "upgradeToPremium":
return "Upgrade To Premium";
return "Upgrade to Premium";
default:
return key;
}
@@ -115,3 +133,18 @@ export default {
type Story = StoryObj<PremiumUpgradeDialogComponent>;
export const Default: Story = {};
export const NoPricingData: Story = {
decorators: [
moduleMetadata({
providers: [
{
provide: SubscriptionPricingServiceAbstraction,
useValue: {
getPersonalSubscriptionPricingTiers$: () => of([mockPremiumTierNoPricingData]),
},
},
],
}),
],
};

View File

@@ -3,12 +3,12 @@ import { CommonModule } from "@angular/common";
import { ChangeDetectionStrategy, Component } from "@angular/core";
import { catchError, EMPTY, firstValueFrom, map, Observable } from "rxjs";
import { SubscriptionPricingCardDetails } from "@bitwarden/angular/billing/types/subscription-pricing-card-details";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SubscriptionPricingServiceAbstraction } from "@bitwarden/common/billing/abstractions/subscription-pricing.service.abstraction";
import {
PersonalSubscriptionPricingTier,
PersonalSubscriptionPricingTierIds,
SubscriptionCadence,
SubscriptionCadenceIds,
} from "@bitwarden/common/billing/types/subscription-pricing-tier";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@@ -16,7 +16,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
ButtonModule,
ButtonType,
CenterPositionStrategy,
DialogModule,
DialogRef,
@@ -27,14 +26,6 @@ import {
} from "@bitwarden/components";
import { LogService } from "@bitwarden/logging";
type CardDetails = {
title: string;
tagline: string;
price: { amount: number; cadence: SubscriptionCadence };
button: { text: string; type: ButtonType; icon?: { type: string; position: "before" | "after" } };
features: string[];
};
@Component({
selector: "billing-premium-upgrade-dialog",
standalone: true,
@@ -51,9 +42,8 @@ type CardDetails = {
templateUrl: "./premium-upgrade-dialog.component.html",
})
export class PremiumUpgradeDialogComponent {
protected cardDetails$: Observable<CardDetails | null> = this.subscriptionPricingService
.getPersonalSubscriptionPricingTiers$()
.pipe(
protected cardDetails$: Observable<SubscriptionPricingCardDetails | null> =
this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$().pipe(
map((tiers) => tiers.find((tier) => tier.id === PersonalSubscriptionPricingTierIds.Premium)),
map((tier) => this.mapPremiumTierToCardDetails(tier!)),
catchError((error: unknown) => {
@@ -91,14 +81,18 @@ export class PremiumUpgradeDialogComponent {
this.dialogRef.close();
}
private mapPremiumTierToCardDetails(tier: PersonalSubscriptionPricingTier): CardDetails {
private mapPremiumTierToCardDetails(
tier: PersonalSubscriptionPricingTier,
): SubscriptionPricingCardDetails {
return {
title: tier.name,
tagline: tier.description,
price: {
amount: tier.passwordManager.annualPrice / 12,
cadence: SubscriptionCadenceIds.Monthly,
},
price: tier.passwordManager.annualPrice
? {
amount: tier.passwordManager.annualPrice / 12,
cadence: SubscriptionCadenceIds.Monthly,
}
: undefined,
button: {
text: this.i18nService.t("upgradeNow"),
type: "primary",

View File

@@ -5,6 +5,7 @@ import { firstValueFrom, Observable, switchMap } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -16,6 +17,7 @@ import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/com
export class PremiumComponent implements OnInit {
isPremium$: Observable<boolean>;
price = 10;
storageProvidedGb = 0;
refreshPromise: Promise<any>;
cloudWebVaultUrl: string;
@@ -29,6 +31,7 @@ export class PremiumComponent implements OnInit {
billingAccountProfileStateService: BillingAccountProfileStateService,
private toastService: ToastService,
accountService: AccountService,
private billingApiService: BillingApiServiceAbstraction,
) {
this.isPremium$ = accountService.activeAccount$.pipe(
switchMap((account) =>
@@ -39,6 +42,9 @@ export class PremiumComponent implements OnInit {
async ngOnInit() {
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
const premiumResponse = await this.billingApiService.getPremiumPlan();
this.storageProvidedGb = premiumResponse.storage.provided;
this.price = premiumResponse.seat.price;
}
async refresh() {

View File

@@ -0,0 +1,10 @@
import { SubscriptionCadence } from "@bitwarden/common/billing/types/subscription-pricing-tier";
import { ButtonType } from "@bitwarden/components";
export type SubscriptionPricingCardDetails = {
title: string;
tagline: string;
price?: { amount: number; cadence: SubscriptionCadence };
button: { text: string; type: ButtonType; icon?: { type: string; position: "before" | "after" } };
features: string[];
};

View File

@@ -1,26 +0,0 @@
<bit-callout [icon]="icon" [title]="title" [type]="$any(type)" [useAlertRole]="useAlertRole">
<div class="tw-pl-7 tw-m-0" *ngIf="enforcedPolicyOptions">
{{ enforcedPolicyMessage }}
<ul>
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
{{ "policyInEffectMinComplexity" | i18n: getPasswordScoreAlertDisplay() }}
</li>
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
{{ "policyInEffectMinLength" | i18n: enforcedPolicyOptions?.minLength.toString() }}
</li>
<li *ngIf="enforcedPolicyOptions?.requireUpper">
{{ "policyInEffectUppercase" | i18n }}
</li>
<li *ngIf="enforcedPolicyOptions?.requireLower">
{{ "policyInEffectLowercase" | i18n }}
</li>
<li *ngIf="enforcedPolicyOptions?.requireNumbers">
{{ "policyInEffectNumbers" | i18n }}
</li>
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
{{ "policyInEffectSpecial" | i18n: "!@#$%^&*" }}
</li>
</ul>
</div>
<ng-content></ng-content>
</bit-callout>

View File

@@ -1,70 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Input, OnInit } from "@angular/core";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CalloutTypes } from "@bitwarden/components";
/**
* @deprecated use the CL's `CalloutComponent` instead
*/
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "app-callout",
templateUrl: "callout.component.html",
standalone: false,
})
export class DeprecatedCalloutComponent implements OnInit {
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() type: CalloutTypes = "info";
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() icon: string;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() title: string;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() enforcedPolicyOptions: MasterPasswordPolicyOptions;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() enforcedPolicyMessage: string;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() useAlertRole = false;
calloutStyle: string;
constructor(private i18nService: I18nService) {}
ngOnInit() {
this.calloutStyle = this.type;
if (this.enforcedPolicyMessage === undefined) {
this.enforcedPolicyMessage = this.i18nService.t("masterPasswordPolicyInEffect");
}
}
getPasswordScoreAlertDisplay() {
if (this.enforcedPolicyOptions == null) {
return "";
}
let str: string;
switch (this.enforcedPolicyOptions.minComplexity) {
case 4:
str = this.i18nService.t("strong");
break;
case 3:
str = this.i18nService.t("good");
break;
default:
str = this.i18nService.t("weak");
break;
}
return str + " (" + this.enforcedPolicyOptions.minComplexity + ")";
}
}

View File

@@ -1,4 +1,4 @@
import { InjectFlags, InjectOptions, Injector, ProviderToken } from "@angular/core";
import { InjectOptions, Injector, ProviderToken } from "@angular/core";
export class ModalInjector implements Injector {
constructor(
@@ -12,8 +12,8 @@ export class ModalInjector implements Injector {
options: InjectOptions & { optional?: false },
): T;
get<T>(token: ProviderToken<T>, notFoundValue: null, options: InjectOptions): T;
get<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions | InjectFlags): T;
get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
get<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions | null): T;
get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: null): T;
get(token: any, notFoundValue?: any): any;
get(token: any, notFoundValue?: any, flags?: any): any {
return this._additionalTokens.get(token) ?? this._parentInjector.get<any>(token, notFoundValue);

View File

@@ -26,7 +26,6 @@ import {
import { TwoFactorIconComponent } from "./auth/components/two-factor-icon.component";
import { NotPremiumDirective } from "./billing/directives/not-premium.directive";
import { DeprecatedCalloutComponent } from "./components/callout.component";
import { A11yInvalidDirective } from "./directives/a11y-invalid.directive";
import { ApiActionDirective } from "./directives/api-action.directive";
import { BoxRowDirective } from "./directives/box-row.directive";
@@ -86,7 +85,6 @@ import { IconComponent } from "./vault/components/icon.component";
A11yInvalidDirective,
ApiActionDirective,
BoxRowDirective,
DeprecatedCalloutComponent,
CopyTextDirective,
CreditCardNumberPipe,
EllipsisPipe,
@@ -115,7 +113,6 @@ import { IconComponent } from "./vault/components/icon.component";
AutofocusDirective,
ToastModule,
BoxRowDirective,
DeprecatedCalloutComponent,
CopyTextDirective,
CreditCardNumberPipe,
EllipsisPipe,

View File

@@ -38,16 +38,24 @@ export const ENCRYPTED_MIGRATION_DISMISSED = new UserKeyDefinition<Date>(
},
);
const DISMISS_TIME_HOURS = 24;
<<<<<<< HEAD
const VAULT_ROUTE = "/vault";
=======
const VAULT_ROUTES = ["/vault", "/tabs/vault", "/tabs/current"];
>>>>>>> main
/**
* This services schedules encrypted migrations for users on clients that are interactive (non-cli), and handles manual interaction,
* if it is required by showing a UI prompt. It is only one means of triggering migrations, in case the user stays unlocked for a while,
* or regularly logs in without a master-password, when the migrations do require a master-password to run.
*/
<<<<<<< HEAD
export class DefaultEncryptedMigrationsSchedulerService
implements EncryptedMigrationsSchedulerService
{
=======
export class DefaultEncryptedMigrationsSchedulerService implements EncryptedMigrationsSchedulerService {
>>>>>>> main
isMigrating = false;
url$: Observable<string>;
@@ -87,7 +95,11 @@ export class DefaultEncryptedMigrationsSchedulerService
]).pipe(
filter(
([authStatus, _date, url]) =>
<<<<<<< HEAD
authStatus === AuthenticationStatus.Unlocked && url === VAULT_ROUTE,
=======
authStatus === AuthenticationStatus.Unlocked && VAULT_ROUTES.includes(url),
>>>>>>> main
),
concatMap(() => this.runMigrationsIfNeeded(userId)),
),

View File

@@ -184,7 +184,12 @@ import { DefaultChangeKdfApiService } from "@bitwarden/common/key-management/kdf
import { ChangeKdfApiService } from "@bitwarden/common/key-management/kdf/change-kdf-api.service.abstraction";
import { DefaultChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf.service";
import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf.service.abstraction";
<<<<<<< HEAD
=======
import { KeyConnectorApiService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector-api.service";
>>>>>>> main
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import { DefaultKeyConnectorApiService } from "@bitwarden/common/key-management/key-connector/services/default-key-connector-api.service";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service";
import { KeyApiService } from "@bitwarden/common/key-management/keys/services/abstractions/key-api-service.abstraction";
import { RotateableKeySetService } from "@bitwarden/common/key-management/keys/services/abstractions/rotateable-key-set.service";
@@ -207,6 +212,7 @@ import {
SendPasswordService,
DefaultSendPasswordService,
} from "@bitwarden/common/key-management/sends";
import { SessionTimeoutTypeService } from "@bitwarden/common/key-management/session-timeout";
import {
DefaultVaultTimeoutService,
DefaultVaultTimeoutSettingsService,
@@ -226,6 +232,7 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
@@ -264,6 +271,7 @@ import { FileUploadService } from "@bitwarden/common/platform/services/file-uplo
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service";
import { DefaultRegisterSdkService } from "@bitwarden/common/platform/services/sdk/register-sdk.service";
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
@@ -910,6 +918,7 @@ const safeProviders: SafeProvider[] = [
StateProvider,
LogService,
DEFAULT_VAULT_TIMEOUT,
SessionTimeoutTypeService,
],
}),
safeProvider({
@@ -946,7 +955,7 @@ const safeProviders: SafeProvider[] = [
deps: [
FolderServiceAbstraction,
CipherServiceAbstraction,
PinServiceAbstraction,
KeyGenerationService,
KeyService,
EncryptService,
CryptoFunctionServiceAbstraction,
@@ -966,7 +975,7 @@ const safeProviders: SafeProvider[] = [
deps: [
CipherServiceAbstraction,
VaultExportApiService,
PinServiceAbstraction,
KeyGenerationService,
KeyService,
EncryptService,
CryptoFunctionServiceAbstraction,
@@ -1047,6 +1056,7 @@ const safeProviders: SafeProvider[] = [
WebPushConnectionService,
AuthRequestAnsweringServiceAbstraction,
ConfigService,
InternalPolicyService,
],
}),
safeProvider({
@@ -1085,7 +1095,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: InternalPolicyService,
useClass: DefaultPolicyService,
deps: [StateProvider, OrganizationServiceAbstraction],
deps: [StateProvider, OrganizationServiceAbstraction, AccountServiceAbstraction],
}),
safeProvider({
provide: PolicyServiceAbstraction,
@@ -1350,16 +1360,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: PinServiceAbstraction,
useClass: PinService,
deps: [
AccountServiceAbstraction,
EncryptService,
KdfConfigService,
KeyGenerationService,
LogService,
KeyService,
SdkService,
PinStateServiceAbstraction,
],
deps: [EncryptService, LogService, KeyService, SdkService, PinStateServiceAbstraction],
}),
safeProvider({
provide: WebAuthnLoginPrfKeyServiceAbstraction,
@@ -1493,7 +1494,13 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: SubscriptionPricingServiceAbstraction,
useClass: DefaultSubscriptionPricingService,
deps: [BillingApiServiceAbstraction, ConfigService, I18nServiceAbstraction, LogService],
deps: [
BillingApiServiceAbstraction,
ConfigService,
I18nServiceAbstraction,
LogService,
EnvironmentService,
],
}),
safeProvider({
provide: OrganizationManagementPreferencesService,
@@ -1609,6 +1616,19 @@ const safeProviders: SafeProvider[] = [
SsoLoginServiceAbstraction,
],
}),
safeProvider({
provide: RegisterSdkService,
useClass: DefaultRegisterSdkService,
deps: [
SdkClientFactory,
EnvironmentService,
PlatformUtilsServiceAbstraction,
AccountServiceAbstraction,
ApiServiceAbstraction,
StateProvider,
ConfigService,
],
}),
safeProvider({
provide: SdkService,
useClass: DefaultSdkService,
@@ -1811,6 +1831,11 @@ const safeProviders: SafeProvider[] = [
useClass: IpcSessionRepository,
deps: [StateProvider],
}),
safeProvider({
provide: KeyConnectorApiService,
useClass: DefaultKeyConnectorApiService,
deps: [ApiServiceAbstraction],
}),
safeProvider({
provide: PremiumInterestStateService,
useClass: NoopPremiumInterestStateService,

View File

@@ -1,9 +1,5 @@
<!-- Applying width and height styles directly to synchronize icon sizing between web/browser/desktop -->
<div
class="tw-flex tw-justify-center tw-items-center"
[ngStyle]="coloredIcon() ? { width: '36px', height: '36px' } : {}"
aria-hidden="true"
>
<div class="tw-flex tw-justify-center tw-items-center" [ngStyle]="iconStyle()" aria-hidden="true">
<ng-container *ngIf="data$ | async as data">
@if (data.imageEnabled && data.image) {
<img
@@ -16,7 +12,7 @@
'tw-invisible tw-absolute': !imageLoaded(),
'tw-size-6': !coloredIcon(),
}"
[ngStyle]="coloredIcon() ? { width: '36px', height: '36px' } : {}"
[ngStyle]="iconStyle()"
(load)="imageLoaded.set(true)"
(error)="imageLoaded.set(false)"
/>
@@ -28,7 +24,7 @@
'tw-bg-illustration-bg-primary tw-rounded-full':
data.icon?.startsWith('bwi-') && coloredIcon(),
}"
[ngStyle]="coloredIcon() ? { width: '36px', height: '36px' } : {}"
[ngStyle]="iconStyle()"
>
<i
class="tw-text-muted bwi bwi-lg {{ data.icon }}"
@@ -36,6 +32,7 @@
color: coloredIcon() ? 'rgb(var(--color-illustration-outline))' : null,
width: data.icon?.startsWith('credit-card') && coloredIcon() ? '36px' : null,
height: data.icon?.startsWith('credit-card') && coloredIcon() ? '30px' : null,
fontSize: size() ? size() + 'px' : null,
}"
></i>
</div>

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, input, signal } from "@angular/core";
import { ChangeDetectionStrategy, Component, computed, input, signal } from "@angular/core";
import { toObservable } from "@angular/core/rxjs-interop";
import {
combineLatest,
@@ -32,8 +32,32 @@ export class IconComponent {
*/
readonly coloredIcon = input<boolean>(false);
/**
* Optional custom size for the icon in pixels.
* When provided, forces explicit dimensions on the icon wrapper to prevent layout collapse at different zoom levels.
* If not provided, the wrapper has no explicit dimensions and relies on CSS classes (tw-size-6/24px for images).
* This can cause the wrapper to collapse when images are loading/hidden, especially at high browser zoom levels.
* Reference: default image size is tw-size-6 (24px), coloredIcon uses 36px.
*/
readonly size = input<number>();
readonly imageLoaded = signal(false);
/**
* Computed style object for icon dimensions.
* Centralizes the sizing logic to avoid repetition in the template.
*/
protected readonly iconStyle = computed(() => {
if (this.coloredIcon()) {
return { width: "36px", height: "36px" };
}
const size = this.size();
if (size) {
return { width: size + "px", height: size + "px" };
}
return {};
});
protected data$: Observable<CipherIconDetails>;
constructor(

View File

@@ -89,7 +89,7 @@ export class VaultFilterComponent implements OnInit {
this.collections = await this.initCollections();
this.showArchiveVaultFilter = await firstValueFrom(
this.cipherArchiveService.hasArchiveFlagEnabled$(),
this.cipherArchiveService.hasArchiveFlagEnabled$,
);
this.isLoaded = true;