mirror of
https://github.com/bitwarden/browser
synced 2026-02-01 01:03:39 +00:00
Merge branch 'main' into km/auto-kdf
This commit is contained in:
@@ -24,6 +24,8 @@ import { KeyService } from "@bitwarden/key-management";
|
||||
selector: "app-user-verification",
|
||||
standalone: false,
|
||||
})
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/PM-28232): Use Directive suffix
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export class UserVerificationComponent implements ControlValueAccessor, OnInit, OnDestroy {
|
||||
private _invalidSecret = false;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<span slot="secondary" class="tw-text-sm">
|
||||
<br />
|
||||
<div>
|
||||
<span class="tw-font-semibold"> {{ "firstLogin" | i18n }}: </span>
|
||||
<span class="tw-font-medium"> {{ "firstLogin" | i18n }}: </span>
|
||||
<span>{{ device.firstLogin | date: "medium" }}</span>
|
||||
</div>
|
||||
</span>
|
||||
@@ -52,7 +52,7 @@
|
||||
}
|
||||
|
||||
<div>
|
||||
<span class="tw-font-semibold">{{ "firstLogin" | i18n }}: </span>
|
||||
<span class="tw-font-medium">{{ "firstLogin" | i18n }}: </span>
|
||||
<span>{{ device.firstLogin | date: "medium" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<div bitTypography="body2">
|
||||
{{ "accessing" | i18n }}:
|
||||
<button [bitMenuTriggerFor]="environmentOptions" bitLink type="button">
|
||||
<b class="tw-text-primary-600 tw-font-semibold">{{
|
||||
<b class="tw-text-primary-600 tw-font-medium">{{
|
||||
data.selectedRegion?.domain || ("selfHostedServer" | i18n)
|
||||
}}</b>
|
||||
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
|
||||
|
||||
@@ -182,7 +182,10 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
if (userKey == null) {
|
||||
masterKeyEncryptedUserKey = await this.keyService.makeUserKey(masterKey);
|
||||
} else {
|
||||
masterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(masterKey);
|
||||
masterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(
|
||||
masterKey,
|
||||
userKey,
|
||||
);
|
||||
}
|
||||
|
||||
return masterKeyEncryptedUserKey;
|
||||
@@ -195,10 +198,13 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
userId: UserId,
|
||||
) {
|
||||
const userDecryptionOpts = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
|
||||
);
|
||||
userDecryptionOpts.hasMasterPassword = true;
|
||||
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
|
||||
await this.userDecryptionOptionsService.setUserDecryptionOptionsById(
|
||||
userId,
|
||||
userDecryptionOpts,
|
||||
);
|
||||
await this.kdfConfigService.setKdfConfig(userId, kdfConfig);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
await this.keyService.setUserKey(masterKeyEncryptedUserKey[0], userId);
|
||||
|
||||
@@ -149,7 +149,9 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
|
||||
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
|
||||
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
|
||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
||||
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||
userDecryptionOptionsSubject,
|
||||
);
|
||||
|
||||
setPasswordRequest = new SetPasswordRequest(
|
||||
credentials.newServerMasterKeyHash,
|
||||
@@ -362,7 +364,8 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
|
||||
expect(userDecryptionOptionsService.setUserDecryptionOptionsById).toHaveBeenCalledWith(
|
||||
userId,
|
||||
userDecryptionOptions,
|
||||
);
|
||||
expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig);
|
||||
@@ -560,7 +563,8 @@ describe("DefaultSetInitialPasswordService", () => {
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
|
||||
expect(userDecryptionOptionsService.setUserDecryptionOptionsById).toHaveBeenCalledWith(
|
||||
userId,
|
||||
userDecryptionOptions,
|
||||
);
|
||||
expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig);
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<!-- 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-bold tw-leading-none tw-m-0">{{
|
||||
<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">
|
||||
|
||||
@@ -158,12 +158,7 @@ describe("PremiumUpgradeDialogComponent", () => {
|
||||
});
|
||||
|
||||
describe("upgrade()", () => {
|
||||
it("should launch URI with query parameter for cloud-hosted environments", async () => {
|
||||
mockEnvironmentService.environment$ = of({
|
||||
getWebVaultUrl: () => "https://vault.bitwarden.com",
|
||||
getRegion: () => Region.US,
|
||||
} as any);
|
||||
|
||||
it("should launch URI with query parameter", async () => {
|
||||
await component["upgrade"]();
|
||||
|
||||
expect(mockPlatformUtilsService.launchUri).toHaveBeenCalledWith(
|
||||
@@ -171,34 +166,6 @@ describe("PremiumUpgradeDialogComponent", () => {
|
||||
);
|
||||
expect(mockDialogRef.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should launch URI without query parameter for self-hosted environments", async () => {
|
||||
mockEnvironmentService.environment$ = of({
|
||||
getWebVaultUrl: () => "https://self-hosted.example.com",
|
||||
getRegion: () => Region.SelfHosted,
|
||||
} as any);
|
||||
|
||||
await component["upgrade"]();
|
||||
|
||||
expect(mockPlatformUtilsService.launchUri).toHaveBeenCalledWith(
|
||||
"https://self-hosted.example.com/#/settings/subscription/premium",
|
||||
);
|
||||
expect(mockDialogRef.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should launch URI with query parameter for EU cloud region", async () => {
|
||||
mockEnvironmentService.environment$ = of({
|
||||
getWebVaultUrl: () => "https://vault.bitwarden.eu",
|
||||
getRegion: () => Region.EU,
|
||||
} as any);
|
||||
|
||||
await component["upgrade"]();
|
||||
|
||||
expect(mockPlatformUtilsService.launchUri).toHaveBeenCalledWith(
|
||||
"https://vault.bitwarden.eu/#/settings/subscription/premium?callToAction=upgradeToPremium",
|
||||
);
|
||||
expect(mockDialogRef.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should close dialog when close button clicked", () => {
|
||||
|
||||
@@ -11,15 +11,13 @@ import {
|
||||
SubscriptionCadence,
|
||||
SubscriptionCadenceIds,
|
||||
} from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
||||
import {
|
||||
EnvironmentService,
|
||||
Region,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
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,
|
||||
ButtonType,
|
||||
CenterPositionStrategy,
|
||||
DialogModule,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
@@ -82,10 +80,9 @@ export class PremiumUpgradeDialogComponent {
|
||||
|
||||
protected async upgrade(): Promise<void> {
|
||||
const environment = await firstValueFrom(this.environmentService.environment$);
|
||||
let vaultUrl = environment.getWebVaultUrl() + "/#/settings/subscription/premium";
|
||||
if (environment.getRegion() !== Region.SelfHosted) {
|
||||
vaultUrl += "?callToAction=upgradeToPremium";
|
||||
}
|
||||
const vaultUrl =
|
||||
environment.getWebVaultUrl() +
|
||||
"/#/settings/subscription/premium?callToAction=upgradeToPremium";
|
||||
this.platformUtilsService.launchUri(vaultUrl);
|
||||
this.dialogRef.close();
|
||||
}
|
||||
@@ -118,6 +115,8 @@ export class PremiumUpgradeDialogComponent {
|
||||
* @returns A dialog reference object
|
||||
*/
|
||||
static open(dialogService: DialogService): DialogRef<PremiumUpgradeDialogComponent> {
|
||||
return dialogService.open(PremiumUpgradeDialogComponent);
|
||||
return dialogService.open(PremiumUpgradeDialogComponent, {
|
||||
positionStrategy: new CenterPositionStrategy(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ export function _cipherListVirtualScrollStrategyFactory(cipherListDir: CipherLis
|
||||
},
|
||||
],
|
||||
})
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/PM-28232): Use Directive suffix
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export class CipherListVirtualScroll extends CdkFixedSizeVirtualScroll {
|
||||
_scrollStrategy: CipherListVirtualScrollStrategy;
|
||||
|
||||
|
||||
@@ -35,9 +35,6 @@ export const SECURE_STORAGE = new SafeInjectionToken<AbstractStorageService>("SE
|
||||
export const LOGOUT_CALLBACK = new SafeInjectionToken<
|
||||
(logoutReason: LogoutReason, userId?: string) => Promise<void>
|
||||
>("LOGOUT_CALLBACK");
|
||||
export const LOCKED_CALLBACK = new SafeInjectionToken<(userId?: string) => Promise<void>>(
|
||||
"LOCKED_CALLBACK",
|
||||
);
|
||||
export const SUPPORTS_SECURE_STORAGE = new SafeInjectionToken<boolean>("SUPPORTS_SECURE_STORAGE");
|
||||
export const LOCALES_DIRECTORY = new SafeInjectionToken<string>("LOCALES_DIRECTORY");
|
||||
export const SYSTEM_LANGUAGE = new SafeInjectionToken<string>("SYSTEM_LANGUAGE");
|
||||
|
||||
@@ -41,9 +41,11 @@ import {
|
||||
AuthRequestService,
|
||||
AuthRequestServiceAbstraction,
|
||||
DefaultAuthRequestApiService,
|
||||
DefaultLockService,
|
||||
DefaultLoginSuccessHandlerService,
|
||||
DefaultLogoutService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
LockService,
|
||||
LoginEmailService,
|
||||
LoginEmailServiceAbstraction,
|
||||
LoginStrategyService,
|
||||
@@ -101,7 +103,6 @@ import { MasterPasswordApiService as MasterPasswordApiServiceAbstraction } from
|
||||
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction";
|
||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { WebAuthnLoginApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-api.service.abstraction";
|
||||
@@ -124,13 +125,17 @@ import { OrganizationInviteService } from "@bitwarden/common/auth/services/organ
|
||||
import { PasswordResetEnrollmentServiceImplementation } from "@bitwarden/common/auth/services/password-reset-enrollment.service.implementation";
|
||||
import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/services/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
|
||||
import { WebAuthnLoginApiService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-api.service";
|
||||
import { WebAuthnLoginPrfKeyService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-prf-key.service";
|
||||
import { WebAuthnLoginService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login.service";
|
||||
import { TwoFactorApiService, DefaultTwoFactorApiService } from "@bitwarden/common/auth/two-factor";
|
||||
import {
|
||||
TwoFactorApiService,
|
||||
DefaultTwoFactorApiService,
|
||||
TwoFactorService,
|
||||
DefaultTwoFactorService,
|
||||
} from "@bitwarden/common/auth/two-factor";
|
||||
import {
|
||||
AutofillSettingsService,
|
||||
AutofillSettingsServiceAbstraction,
|
||||
@@ -162,6 +167,7 @@ import { OrganizationSponsorshipApiService } from "@bitwarden/common/billing/ser
|
||||
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
|
||||
import { DefaultSubscriptionPricingService } from "@bitwarden/common/billing/services/subscription-pricing.service";
|
||||
import { HibpApiService } from "@bitwarden/common/dirt/services/hibp-api.service";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import {
|
||||
DefaultKeyGenerationService,
|
||||
KeyGenerationService,
|
||||
@@ -181,7 +187,9 @@ import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kd
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.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";
|
||||
import { DefaultKeyApiService } from "@bitwarden/common/key-management/keys/services/default-key-api-service.service";
|
||||
import { DefaultRotateableKeySetService } from "@bitwarden/common/key-management/keys/services/default-rotateable-key-set.service";
|
||||
import { MasterPasswordUnlockService } from "@bitwarden/common/key-management/master-password/abstractions/master-password-unlock.service";
|
||||
import {
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
@@ -222,9 +230,11 @@ import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sd
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
|
||||
import { ValidationService as ValidationServiceAbstraction } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { ActionsService } from "@bitwarden/common/platform/actions";
|
||||
import { UnsupportedActionsService } from "@bitwarden/common/platform/actions/unsupported-actions.service";
|
||||
import { IpcSessionRepository } from "@bitwarden/common/platform/ipc";
|
||||
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
|
||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||
@@ -285,6 +295,7 @@ import {
|
||||
} from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { CipherRiskService } from "@bitwarden/common/vault/abstractions/cipher-risk.service";
|
||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||
@@ -306,6 +317,7 @@ import {
|
||||
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
||||
import { DefaultCipherArchiveService } from "@bitwarden/common/vault/services/default-cipher-archive.service";
|
||||
import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service";
|
||||
import { DefaultCipherRiskService } from "@bitwarden/common/vault/services/default-cipher-risk.service";
|
||||
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
|
||||
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
|
||||
@@ -407,7 +419,6 @@ import {
|
||||
HTTP_OPERATIONS,
|
||||
INTRAPROCESS_MESSAGING_SUBJECT,
|
||||
LOCALES_DIRECTORY,
|
||||
LOCKED_CALLBACK,
|
||||
LOG_MAC_FAILURES,
|
||||
LOGOUT_CALLBACK,
|
||||
OBSERVABLE_DISK_STORAGE,
|
||||
@@ -463,10 +474,6 @@ const safeProviders: SafeProvider[] = [
|
||||
},
|
||||
deps: [MessagingServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LOCKED_CALLBACK,
|
||||
useValue: null,
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LOG_MAC_FAILURES,
|
||||
useValue: true,
|
||||
@@ -546,7 +553,7 @@ const safeProviders: SafeProvider[] = [
|
||||
KeyConnectorServiceAbstraction,
|
||||
EnvironmentService,
|
||||
StateServiceAbstraction,
|
||||
TwoFactorServiceAbstraction,
|
||||
TwoFactorService,
|
||||
I18nServiceAbstraction,
|
||||
EncryptService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
@@ -628,6 +635,11 @@ const safeProviders: SafeProvider[] = [
|
||||
MessagingServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: CipherRiskService,
|
||||
useClass: DefaultCipherRiskService,
|
||||
deps: [SdkService, CipherServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: InternalFolderService,
|
||||
useClass: FolderService,
|
||||
@@ -695,7 +707,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
useClass: UserDecryptionOptionsService,
|
||||
deps: [StateProvider],
|
||||
deps: [SingleUserStateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: UserDecryptionOptionsServiceAbstraction,
|
||||
@@ -905,22 +917,12 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultVaultTimeoutService,
|
||||
deps: [
|
||||
AccountServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
CipherServiceAbstraction,
|
||||
FolderServiceAbstraction,
|
||||
CollectionService,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
SearchServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
TokenServiceAbstraction,
|
||||
AuthServiceAbstraction,
|
||||
VaultTimeoutSettingsService,
|
||||
StateEventRunnerService,
|
||||
TaskSchedulerService,
|
||||
LogService,
|
||||
BiometricsService,
|
||||
LOCKED_CALLBACK,
|
||||
LockService,
|
||||
LogoutService,
|
||||
],
|
||||
}),
|
||||
@@ -1112,7 +1114,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: MasterPasswordUnlockService,
|
||||
useClass: DefaultMasterPasswordUnlockService,
|
||||
deps: [InternalMasterPasswordServiceAbstraction, KeyService],
|
||||
deps: [InternalMasterPasswordServiceAbstraction, KeyService, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: KeyConnectorServiceAbstraction,
|
||||
@@ -1189,9 +1191,14 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TwoFactorServiceAbstraction,
|
||||
useClass: TwoFactorService,
|
||||
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction, GlobalStateProvider],
|
||||
provide: TwoFactorService,
|
||||
useClass: DefaultTwoFactorService,
|
||||
deps: [
|
||||
I18nServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
GlobalStateProvider,
|
||||
TwoFactorApiService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: FormValidationErrorsServiceAbstraction,
|
||||
@@ -1308,6 +1315,7 @@ const safeProviders: SafeProvider[] = [
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
LogService,
|
||||
ConfigService,
|
||||
AccountServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -1475,7 +1483,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: OrganizationMetadataServiceAbstraction,
|
||||
useClass: DefaultOrganizationMetadataService,
|
||||
deps: [BillingApiServiceAbstraction, ConfigService],
|
||||
deps: [BillingApiServiceAbstraction, ConfigService, PlatformUtilsServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: BillingAccountProfileStateService,
|
||||
@@ -1757,6 +1765,27 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [EncryptedMigrationsSchedulerService],
|
||||
multi: true,
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LockService,
|
||||
useClass: DefaultLockService,
|
||||
deps: [
|
||||
AccountService,
|
||||
BiometricsService,
|
||||
VaultTimeoutSettingsService,
|
||||
LogoutService,
|
||||
MessagingServiceAbstraction,
|
||||
SearchServiceAbstraction,
|
||||
FolderServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
StateEventRunnerService,
|
||||
CipherServiceAbstraction,
|
||||
AuthServiceAbstraction,
|
||||
SystemService,
|
||||
ProcessReloadServiceAbstraction,
|
||||
LogService,
|
||||
KeyService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: CipherArchiveService,
|
||||
useClass: DefaultCipherArchiveService,
|
||||
@@ -1767,11 +1796,21 @@ const safeProviders: SafeProvider[] = [
|
||||
ConfigService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: RotateableKeySetService,
|
||||
useClass: DefaultRotateableKeySetService,
|
||||
deps: [KeyService, EncryptService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: NewDeviceVerificationComponentService,
|
||||
useClass: DefaultNewDeviceVerificationComponentService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: IpcSessionRepository,
|
||||
useClass: IpcSessionRepository,
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PremiumInterestStateService,
|
||||
useClass: NoopPremiumInterestStateService,
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
>
|
||||
<div class="tw-flex tw-justify-between tw-items-start tw-flex-grow">
|
||||
<div>
|
||||
<h2 bitTypography="h4" class="tw-font-semibold !tw-mb-1">{{ title }}</h2>
|
||||
<h2 *ngIf="title()" bitTypography="h4" class="tw-font-medium !tw-mb-1">{{ title() }}</h2>
|
||||
<p
|
||||
*ngIf="subtitle"
|
||||
*ngIf="subtitle()"
|
||||
class="tw-text-main tw-mb-0"
|
||||
bitTypography="body2"
|
||||
[innerHTML]="subtitle"
|
||||
[innerHTML]="subtitle()"
|
||||
></p>
|
||||
<ng-content *ngIf="!subtitle"></ng-content>
|
||||
<ng-content *ngIf="!subtitle()"></ng-content>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
size="small"
|
||||
*ngIf="!persistent"
|
||||
*ngIf="!persistent()"
|
||||
(click)="handleDismiss()"
|
||||
class="-tw-me-2"
|
||||
[label]="'close' | i18n"
|
||||
@@ -28,10 +28,10 @@
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="primary"
|
||||
*ngIf="buttonText"
|
||||
*ngIf="buttonText()"
|
||||
(click)="handleButtonClick($event)"
|
||||
>
|
||||
{{ buttonText }}
|
||||
<i *ngIf="buttonIcon" [ngClass]="buttonIcon" class="bwi tw-ml-1" aria-hidden="true"></i>
|
||||
{{ buttonText() }}
|
||||
<i *ngIf="buttonIcon()" [ngClass]="buttonIcon()" class="bwi tw-ml-1" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { SpotlightComponent } from "./spotlight.component";
|
||||
|
||||
describe("SpotlightComponent", () => {
|
||||
let fixture: ComponentFixture<SpotlightComponent>;
|
||||
let component: SpotlightComponent;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SpotlightComponent],
|
||||
providers: [{ provide: I18nService, useValue: { t: (key: string) => key } }],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SpotlightComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
function detect(): void {
|
||||
fixture.detectChanges();
|
||||
}
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("rendering when inputs are null", () => {
|
||||
it("should render without crashing when inputs are null/undefined", () => {
|
||||
// Explicitly drive the inputs to null to exercise template null branches
|
||||
fixture.componentRef.setInput("title", null);
|
||||
fixture.componentRef.setInput("subtitle", null);
|
||||
fixture.componentRef.setInput("buttonText", null);
|
||||
fixture.componentRef.setInput("buttonIcon", null);
|
||||
// persistent has a default, but drive it as well for coverage sanity
|
||||
fixture.componentRef.setInput("persistent", false);
|
||||
|
||||
expect(() => detect()).not.toThrow();
|
||||
|
||||
const root = fixture.debugElement.nativeElement as HTMLElement;
|
||||
expect(root).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("close button visibility based on persistent", () => {
|
||||
it("should show the close button when persistent is false", () => {
|
||||
fixture.componentRef.setInput("persistent", false);
|
||||
detect();
|
||||
|
||||
// Assumes dismiss uses bitIconButton
|
||||
const dismissButton = fixture.debugElement.query(By.css("button[bitIconButton]"));
|
||||
|
||||
expect(dismissButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should hide the close button when persistent is true", () => {
|
||||
fixture.componentRef.setInput("persistent", true);
|
||||
detect();
|
||||
|
||||
const dismissButton = fixture.debugElement.query(By.css("button[bitIconButton]"));
|
||||
expect(dismissButton).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("event emission", () => {
|
||||
it("should emit onButtonClick when CTA button is clicked", () => {
|
||||
const clickSpy = jest.fn();
|
||||
component.onButtonClick.subscribe(clickSpy);
|
||||
|
||||
fixture.componentRef.setInput("buttonText", "Click me");
|
||||
detect();
|
||||
|
||||
const buttonDe = fixture.debugElement.query(By.css("button[bitButton]"));
|
||||
expect(buttonDe).toBeTruthy();
|
||||
|
||||
const event = new MouseEvent("click");
|
||||
buttonDe.triggerEventHandler("click", event);
|
||||
|
||||
expect(clickSpy).toHaveBeenCalledTimes(1);
|
||||
expect(clickSpy.mock.calls[0][0]).toBeInstanceOf(MouseEvent);
|
||||
});
|
||||
|
||||
it("should emit onDismiss when close button is clicked", () => {
|
||||
const dismissSpy = jest.fn();
|
||||
component.onDismiss.subscribe(dismissSpy);
|
||||
|
||||
fixture.componentRef.setInput("persistent", false);
|
||||
detect();
|
||||
|
||||
const dismissButton = fixture.debugElement.query(By.css("button[bitIconButton]"));
|
||||
expect(dismissButton).toBeTruthy();
|
||||
|
||||
dismissButton.triggerEventHandler("click", new MouseEvent("click"));
|
||||
|
||||
expect(dismissSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("handleButtonClick should emit via onButtonClick()", () => {
|
||||
const clickSpy = jest.fn();
|
||||
component.onButtonClick.subscribe(clickSpy);
|
||||
|
||||
const event = new MouseEvent("click");
|
||||
component.handleButtonClick(event);
|
||||
|
||||
expect(clickSpy).toHaveBeenCalledTimes(1);
|
||||
expect(clickSpy.mock.calls[0][0]).toBe(event);
|
||||
});
|
||||
|
||||
it("handleDismiss should emit via onDismiss()", () => {
|
||||
const dismissSpy = jest.fn();
|
||||
component.onDismiss.subscribe(dismissSpy);
|
||||
|
||||
component.handleDismiss();
|
||||
|
||||
expect(dismissSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("content projection behavior", () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [SpotlightComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<bit-spotlight>
|
||||
<span class="tw-text-sm">Projected content</span>
|
||||
</bit-spotlight>
|
||||
`,
|
||||
})
|
||||
class HostWithProjectionComponent {}
|
||||
|
||||
let hostFixture: ComponentFixture<HostWithProjectionComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
hostFixture = TestBed.createComponent(HostWithProjectionComponent);
|
||||
});
|
||||
|
||||
it("should render projected content inside the spotlight", () => {
|
||||
hostFixture.detectChanges();
|
||||
|
||||
const projected = hostFixture.debugElement.query(By.css(".tw-text-sm"));
|
||||
expect(projected).toBeTruthy();
|
||||
expect(projected.nativeElement.textContent.trim()).toBe("Projected content");
|
||||
});
|
||||
});
|
||||
|
||||
describe("boolean attribute transform for persistent", () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [CommonModule, SpotlightComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<!-- bare persistent attribute -->
|
||||
<bit-spotlight *ngIf="mode === 'bare'" persistent></bit-spotlight>
|
||||
|
||||
<!-- no persistent attribute -->
|
||||
<bit-spotlight *ngIf="mode === 'none'"></bit-spotlight>
|
||||
|
||||
<!-- explicit persistent="false" -->
|
||||
<bit-spotlight *ngIf="mode === 'falseStr'" persistent="false"></bit-spotlight>
|
||||
`,
|
||||
})
|
||||
class BooleanHostComponent {
|
||||
mode: "bare" | "none" | "falseStr" = "bare";
|
||||
}
|
||||
|
||||
let boolFixture: ComponentFixture<BooleanHostComponent>;
|
||||
let boolHost: BooleanHostComponent;
|
||||
|
||||
beforeEach(async () => {
|
||||
boolFixture = TestBed.createComponent(BooleanHostComponent);
|
||||
boolHost = boolFixture.componentInstance;
|
||||
});
|
||||
|
||||
function getSpotlight(): SpotlightComponent {
|
||||
const de = boolFixture.debugElement.query(By.directive(SpotlightComponent));
|
||||
return de.componentInstance as SpotlightComponent;
|
||||
}
|
||||
|
||||
it("treats bare 'persistent' attribute as true via booleanAttribute", () => {
|
||||
boolHost.mode = "bare";
|
||||
boolFixture.detectChanges();
|
||||
|
||||
const spotlight = getSpotlight();
|
||||
expect(spotlight.persistent()).toBe(true);
|
||||
});
|
||||
|
||||
it("uses default false when 'persistent' is omitted", () => {
|
||||
boolHost.mode = "none";
|
||||
boolFixture.detectChanges();
|
||||
|
||||
const spotlight = getSpotlight();
|
||||
expect(spotlight.persistent()).toBe(false);
|
||||
});
|
||||
|
||||
it('treats persistent="false" as false', () => {
|
||||
boolHost.mode = "falseStr";
|
||||
boolFixture.detectChanges();
|
||||
|
||||
const spotlight = getSpotlight();
|
||||
expect(spotlight.persistent()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,43 +1,28 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { booleanAttribute, ChangeDetectionStrategy, Component, input, output } from "@angular/core";
|
||||
|
||||
import { ButtonModule, IconButtonModule, TypographyModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
// 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: "bit-spotlight",
|
||||
templateUrl: "spotlight.component.html",
|
||||
imports: [ButtonModule, CommonModule, IconButtonModule, I18nPipe, TypographyModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SpotlightComponent {
|
||||
// The title of the component
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input({ required: true }) title: string | null = null;
|
||||
readonly title = input<string>();
|
||||
// The subtitle of the component
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() subtitle?: string | null = null;
|
||||
readonly subtitle = input<string>();
|
||||
// The text to display on the button
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() buttonText?: string;
|
||||
// Wheter the component can be dismissed, if true, the component will not show a close button
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() persistent = false;
|
||||
readonly buttonText = input<string>();
|
||||
// Whether the component can be dismissed, if true, the component will not show a close button
|
||||
readonly persistent = input(false, { transform: booleanAttribute });
|
||||
// Optional icon to display on the button
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() buttonIcon: string | null = null;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() onDismiss = new EventEmitter<void>();
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() onButtonClick = new EventEmitter();
|
||||
readonly buttonIcon = input<string>();
|
||||
readonly onDismiss = output<void>();
|
||||
readonly onButtonClick = output<MouseEvent>();
|
||||
|
||||
handleButtonClick(event: MouseEvent): void {
|
||||
this.onButtonClick.emit(event);
|
||||
|
||||
@@ -37,6 +37,7 @@ export const NudgeType = {
|
||||
NewNoteItemStatus: "new-note-item-status",
|
||||
NewSshItemStatus: "new-ssh-item-status",
|
||||
GeneratorNudgeStatus: "generator-nudge-status",
|
||||
PremiumUpgrade: "premium-upgrade",
|
||||
} as const;
|
||||
|
||||
export type NudgeType = UnionOfValues<typeof NudgeType>;
|
||||
|
||||
@@ -88,14 +88,10 @@ export class VaultFilterComponent implements OnInit {
|
||||
this.folders$ = await this.vaultFilterService.buildNestedFolders();
|
||||
this.collections = await this.initCollections();
|
||||
|
||||
const userCanArchive = await firstValueFrom(
|
||||
this.cipherArchiveService.userCanArchive$(this.activeUserId),
|
||||
);
|
||||
const showArchiveVault = await firstValueFrom(
|
||||
this.cipherArchiveService.showArchiveVault$(this.activeUserId),
|
||||
this.showArchiveVaultFilter = await firstValueFrom(
|
||||
this.cipherArchiveService.hasArchiveFlagEnabled$(),
|
||||
);
|
||||
|
||||
this.showArchiveVaultFilter = userCanArchive || showArchiveVault;
|
||||
this.isLoaded = true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user