From 4c347d09ac6415e31eba5564565a799a86cf6481 Mon Sep 17 00:00:00 2001 From: neuronull <9162534+neuronull@users.noreply.github.com> Date: Tue, 21 Oct 2025 07:22:09 -0700 Subject: [PATCH 01/16] Fix biometric v2 windows unit test clippy lint (#16961) --- apps/desktop/desktop_native/core/src/biometric_v2/windows.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/desktop/desktop_native/core/src/biometric_v2/windows.rs b/apps/desktop/desktop_native/core/src/biometric_v2/windows.rs index 479f96312ce..043c2453cd0 100644 --- a/apps/desktop/desktop_native/core/src/biometric_v2/windows.rs +++ b/apps/desktop/desktop_native/core/src/biometric_v2/windows.rs @@ -376,6 +376,7 @@ unsafe fn as_mut_bytes(buffer: &IBuffer) -> Result<&mut [u8]> { } #[cfg(test)] +#[allow(clippy::print_stdout)] mod tests { use crate::biometric_v2::{ biometric_v2::{ From f1340c67da7fe069be9e848b6d7188b829571c4c Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 21 Oct 2025 15:46:15 +0100 Subject: [PATCH 02/16] [PM-27050] Fix : Cannot upload self-hosted license on new Premium page (#16919) * Fix the isselfhost bug * Resolve the self-host issue --- .../individual-billing-routing.module.ts | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/web/src/app/billing/individual/individual-billing-routing.module.ts b/apps/web/src/app/billing/individual/individual-billing-routing.module.ts index 0bc6b1effbb..26d0c43ff8f 100644 --- a/apps/web/src/app/billing/individual/individual-billing-routing.module.ts +++ b/apps/web/src/app/billing/individual/individual-billing-routing.module.ts @@ -1,8 +1,11 @@ -import { NgModule } from "@angular/core"; +import { inject, NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { map } from "rxjs"; -import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; +import { componentRouteSwap } from "@bitwarden/angular/utils/component-route-swap"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { AccountPaymentDetailsComponent } from "@bitwarden/web-vault/app/billing/individual/payment-details/account-payment-details.component"; import { BillingHistoryViewComponent } from "./billing-history-view.component"; @@ -23,15 +26,22 @@ const routes: Routes = [ component: UserSubscriptionComponent, data: { titleId: "premiumMembership" }, }, - ...featureFlaggedRoute({ - defaultComponent: PremiumComponent, - flaggedComponent: PremiumVNextComponent, - featureFlag: FeatureFlag.PM24033PremiumUpgradeNewDesign, - routeOptions: { + ...componentRouteSwap( + PremiumComponent, + PremiumVNextComponent, + () => { + const configService = inject(ConfigService); + const platformUtilsService = inject(PlatformUtilsService); + + return configService + .getFeatureFlag$(FeatureFlag.PM24033PremiumUpgradeNewDesign) + .pipe(map((flagValue) => flagValue === true && !platformUtilsService.isSelfHost())); + }, + { data: { titleId: "goPremium" }, path: "premium", }, - }), + ), { path: "payment-details", component: AccountPaymentDetailsComponent, From a5dd42396cdf5b8beda4aa242c103157d5aa3bc6 Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Tue, 21 Oct 2025 11:02:44 -0400 Subject: [PATCH 03/16] [PM-27024] password progress card at risk detection (#16955) * [PM-27024] Fix password change progress card to track only critical apps and detect new at-risk passwords - Filter at-risk password count to critical applications only - Update state logic to transition back to assign tasks when new at-risk passwords detected - Only create security tasks for critical applications with at-risk passwords - Show 'X new passwords at-risk' message when tasks exist and new at-risk passwords appear * spec --- apps/web/src/locales/en/messages.json | 9 +++ .../services/all-activities.service.ts | 4 +- .../password-change-metric.component.html | 6 +- .../password-change-metric.component.ts | 68 +++++++++++++------ .../shared/security-tasks.service.spec.ts | 20 ++++++ .../shared/security-tasks.service.ts | 3 +- 6 files changed, 87 insertions(+), 23 deletions(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 5eab55e2301..1f8c4ec55b2 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -154,6 +154,15 @@ } } }, + "newPasswordsAtRisk": { + "message": "$COUNT$ new passwords at-risk", + "placeholders": { + "count": { + "content": "$1", + "example": "5" + } + } + }, "notifiedMembersWithCount": { "message": "Notified members ($COUNT$)", "placeholders": { diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/all-activities.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/all-activities.service.ts index ee7f0fae56a..4044a01926c 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/all-activities.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/all-activities.service.ts @@ -76,7 +76,9 @@ export class AllActivitiesService { } setAllAppsReportDetails(applications: ApplicationHealthReportDetailEnriched[]) { - const totalAtRiskPasswords = applications.reduce( + // Only count at-risk passwords for CRITICAL applications + const criticalApps = applications.filter((app) => app.isMarkedAsCritical); + const totalAtRiskPasswords = criticalApps.reduce( (sum, app) => sum + app.atRiskPasswordCount, 0, ); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.html index 718efc4c67e..674bc0b5c62 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.html @@ -21,7 +21,11 @@
- {{ "countOfAtRiskPasswords" | i18n: atRiskPasswordsCount }} + {{ + hasExistingTasks + ? ("newPasswordsAtRisk" | i18n: newAtRiskPasswordsCount) + : ("countOfAtRiskPasswords" | i18n: atRiskPasswordsCount) + }}
diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.ts index 3b8475ed5cf..4d2085d7c3a 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity-cards/password-change-metric.component.ts @@ -9,6 +9,7 @@ import { LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher, SecurityTasksApiService, TaskMetrics, + OrganizationReportSummary, } from "@bitwarden/bit-common/dirt/reports/risk-insights"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { ButtonModule, ProgressModule, TypographyModule } from "@bitwarden/components"; @@ -73,25 +74,8 @@ export class PasswordChangeMetricComponent implements OnInit { this.totalTasks = taskMetrics.totalTasks; this.allApplicationsDetails = allApplicationsDetails; - // No critical apps setup - this.renderMode = - summary.totalCriticalApplicationCount === 0 ? RenderMode.noCriticalApps : this.renderMode; - - // Critical apps setup with at-risk apps but no tasks - this.renderMode = - summary.totalCriticalApplicationCount > 0 && - summary.totalCriticalAtRiskApplicationCount >= 0 && - taskMetrics.totalTasks === 0 - ? RenderMode.criticalAppsWithAtRiskAppsAndNoTasks - : this.renderMode; - - // Critical apps setup with at-risk apps and tasks - this.renderMode = - summary.totalAtRiskApplicationCount > 0 && - summary.totalCriticalAtRiskApplicationCount >= 0 && - taskMetrics.totalTasks > 0 - ? RenderMode.criticalAppsWithAtRiskAppsAndTasks - : this.renderMode; + // Determine render mode based on state + this.renderMode = this.determineRenderMode(summary, taskMetrics, atRiskPasswordsCount); this.allActivitiesService.setPasswordChangeProgressMetricHasProgressBar( this.renderMode === RenderMode.criticalAppsWithAtRiskAppsAndTasks, @@ -106,6 +90,38 @@ export class PasswordChangeMetricComponent implements OnInit { protected accessIntelligenceSecurityTasksService: AccessIntelligenceSecurityTasksService, ) {} + private determineRenderMode( + summary: OrganizationReportSummary, + taskMetrics: TaskMetrics, + atRiskPasswordsCount: number, + ): RenderMode { + // State 1: No critical apps setup + if (summary.totalCriticalApplicationCount === 0) { + return RenderMode.noCriticalApps; + } + + // State 2: Critical apps with at-risk passwords but no tasks assigned yet + // OR tasks exist but NEW at-risk passwords detected (more at-risk passwords than tasks) + if ( + summary.totalCriticalApplicationCount > 0 && + (taskMetrics.totalTasks === 0 || atRiskPasswordsCount > taskMetrics.totalTasks) + ) { + return RenderMode.criticalAppsWithAtRiskAppsAndNoTasks; + } + + // State 3: Critical apps with at-risk apps and tasks (progress tracking) + if ( + summary.totalCriticalApplicationCount > 0 && + taskMetrics.totalTasks > 0 && + atRiskPasswordsCount <= taskMetrics.totalTasks + ) { + return RenderMode.criticalAppsWithAtRiskAppsAndTasks; + } + + // Default to no critical apps + return RenderMode.noCriticalApps; + } + get completedPercent(): number { if (this.totalTasks === 0) { return 0; @@ -144,7 +160,19 @@ export class PasswordChangeMetricComponent implements OnInit { } get canAssignTasks(): boolean { - return this.atRiskAppsCount > this.totalTasks ? true : false; + return this.atRiskPasswordsCount > this.totalTasks; + } + + get hasExistingTasks(): boolean { + return this.totalTasks > 0; + } + + get newAtRiskPasswordsCount(): number { + // Calculate new at-risk passwords as the difference between current count and tasks created + if (this.atRiskPasswordsCount > this.totalTasks) { + return this.atRiskPasswordsCount - this.totalTasks; + } + return 0; } get renderModes() { diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.spec.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.spec.ts index 520164c80e7..a57bdfc279c 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.spec.ts @@ -40,6 +40,7 @@ describe("AccessIntelligenceSecurityTasksService", () => { const organizationId = "org-1" as OrganizationId; const apps = [ { + isMarkedAsCritical: true, atRiskPasswordCount: 1, atRiskCipherIds: ["cid1"], } as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher, @@ -56,10 +57,12 @@ describe("AccessIntelligenceSecurityTasksService", () => { const organizationId = "org-2" as OrganizationId; const apps = [ { + isMarkedAsCritical: true, atRiskPasswordCount: 2, atRiskCipherIds: ["cid1", "cid2"], } as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher, { + isMarkedAsCritical: true, atRiskPasswordCount: 1, atRiskCipherIds: ["cid2"], } as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher, @@ -85,6 +88,7 @@ describe("AccessIntelligenceSecurityTasksService", () => { const organizationId = "org-3" as OrganizationId; const apps = [ { + isMarkedAsCritical: true, atRiskPasswordCount: 1, atRiskCipherIds: ["cid3"], } as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher, @@ -106,6 +110,7 @@ describe("AccessIntelligenceSecurityTasksService", () => { const organizationId = "org-4" as OrganizationId; const apps = [ { + isMarkedAsCritical: true, atRiskPasswordCount: 0, atRiskCipherIds: ["cid4"], } as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher, @@ -115,5 +120,20 @@ describe("AccessIntelligenceSecurityTasksService", () => { expect(defaultAdminTaskServiceSpy.bulkCreateTasks).toHaveBeenCalledWith(organizationId, []); expect(result).toBe(0); }); + + it("should not create any tasks for non-critical apps", async () => { + const organizationId = "org-5" as OrganizationId; + const apps = [ + { + isMarkedAsCritical: false, + atRiskPasswordCount: 2, + atRiskCipherIds: ["cid5", "cid6"], + } as LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher, + ]; + const result = await service.requestPasswordChange(organizationId, apps); + + expect(defaultAdminTaskServiceSpy.bulkCreateTasks).toHaveBeenCalledWith(organizationId, []); + expect(result).toBe(0); + }); }); }); diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts index c5610c638b0..34fd6daa2b0 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/shared/security-tasks.service.ts @@ -33,8 +33,9 @@ export class AccessIntelligenceSecurityTasksService { organizationId: OrganizationId, apps: LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher[], ): Promise { + // Only create tasks for CRITICAL applications with at-risk passwords const cipherIds = apps - .filter((_) => _.atRiskPasswordCount > 0) + .filter((_) => _.isMarkedAsCritical && _.atRiskPasswordCount > 0) .flatMap((app) => app.atRiskCipherIds); const distinctCipherIds = Array.from(new Set(cipherIds)); From 5a307633bb8d1d7347d67bc3249d0393ca8ec23c Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 21 Oct 2025 17:24:52 +0200 Subject: [PATCH 04/16] [PM-26778] Make VaultTimeoutService use LogoutService (#16820) * Make vaulttimeoutservice use logoutservice * Fix browser build * Fix mv3 build * Fix lint --- apps/browser/src/background/idle.background.ts | 5 ++++- apps/browser/src/background/main.background.ts | 5 ++++- .../foreground-vault-timeout.service.ts | 4 ---- .../key-rotation/user-key-rotation.service.ts | 7 +++---- .../src/services/jslib-services.module.ts | 2 +- .../abstractions/vault-timeout.service.ts | 1 - .../services/vault-timeout.service.spec.ts | 14 +++++++------- .../services/vault-timeout.service.ts | 17 ++++------------- 8 files changed, 23 insertions(+), 32 deletions(-) diff --git a/apps/browser/src/background/idle.background.ts b/apps/browser/src/background/idle.background.ts index 2de4b48a9c0..0f89aa4792a 100644 --- a/apps/browser/src/background/idle.background.ts +++ b/apps/browser/src/background/idle.background.ts @@ -1,5 +1,6 @@ import { firstValueFrom } from "rxjs"; +import { LogoutService } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { VaultTimeoutAction, @@ -8,6 +9,7 @@ import { VaultTimeoutStringType, } from "@bitwarden/common/key-management/vault-timeout"; import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications"; +import { UserId } from "@bitwarden/user-core"; const IdleInterval = 60 * 5; // 5 minutes @@ -21,6 +23,7 @@ export default class IdleBackground { private serverNotificationsService: ServerNotificationsService, private accountService: AccountService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, + private logoutService: LogoutService, ) { this.idle = chrome.idle || (browser != null ? browser.idle : null); } @@ -61,7 +64,7 @@ export default class IdleBackground { this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId), ); if (action === VaultTimeoutAction.LogOut) { - await this.vaultTimeoutService.logOut(userId); + await this.logoutService.logout(userId as UserId, "vaultTimeout"); } else { await this.vaultTimeoutService.lock(userId); } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 7ba55a45892..c4c412732c9 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -21,6 +21,7 @@ import { AuthRequestServiceAbstraction, DefaultAuthRequestApiService, DefaultLockService, + DefaultLogoutService, InternalUserDecryptionOptionsServiceAbstraction, LoginEmailServiceAbstraction, LogoutReason, @@ -976,6 +977,7 @@ export default class MainBackground { this.restrictedItemTypesService, ); + const logoutService = new DefaultLogoutService(this.messagingService); this.vaultTimeoutService = new VaultTimeoutService( this.accountService, this.masterPasswordService, @@ -994,7 +996,7 @@ export default class MainBackground { this.logService, this.biometricsService, lockedCallback, - logoutCallback, + logoutService, ); this.containerService = new ContainerService(this.keyService, this.encryptService); @@ -1386,6 +1388,7 @@ export default class MainBackground { this.serverNotificationsService, this.accountService, this.vaultTimeoutSettingsService, + logoutService, ); this.usernameGenerationService = legacyUsernameGenerationServiceFactory( diff --git a/apps/browser/src/key-management/vault-timeout/foreground-vault-timeout.service.ts b/apps/browser/src/key-management/vault-timeout/foreground-vault-timeout.service.ts index 5003dfd5b29..4081ab03359 100644 --- a/apps/browser/src/key-management/vault-timeout/foreground-vault-timeout.service.ts +++ b/apps/browser/src/key-management/vault-timeout/foreground-vault-timeout.service.ts @@ -13,8 +13,4 @@ export class ForegroundVaultTimeoutService implements BaseVaultTimeoutService { async lock(userId?: UserId): Promise { this.messagingService.send("lockVault", { userId }); } - - async logOut(userId?: string): Promise { - this.messagingService.send("logout", { userId }); - } } diff --git a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts index 0980beddd09..168dbe7442e 100644 --- a/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/key-management/key-rotation/user-key-rotation.service.ts @@ -1,6 +1,7 @@ import { Injectable } from "@angular/core"; import { firstValueFrom, Observable } from "rxjs"; +import { LogoutService } from "@bitwarden/auth/common"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; @@ -14,7 +15,6 @@ import { WrappedPrivateKey, WrappedSigningKey, } from "@bitwarden/common/key-management/types"; -import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -89,7 +89,7 @@ export class UserKeyRotationService { private syncService: SyncService, private webauthnLoginAdminService: WebauthnLoginAdminService, private logService: LogService, - private vaultTimeoutService: VaultTimeoutService, + private logoutService: LogoutService, private toastService: ToastService, private i18nService: I18nService, private dialogService: DialogService, @@ -189,8 +189,7 @@ export class UserKeyRotationService { timeout: 15000, }); - // temporary until userkey can be better verified - await this.vaultTimeoutService.logOut(); + await this.logoutService.logout(user.id); } protected async ensureIsAllowedToRotateUserKey(): Promise { diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index f5642f45b2e..5d2a23444f0 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -891,7 +891,7 @@ const safeProviders: SafeProvider[] = [ LogService, BiometricsService, LOCKED_CALLBACK, - LOGOUT_CALLBACK, + LogoutService, ], }), safeProvider({ diff --git a/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout.service.ts b/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout.service.ts index 1c88a5c51ea..401fb8b107b 100644 --- a/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout.service.ts +++ b/libs/common/src/key-management/vault-timeout/abstractions/vault-timeout.service.ts @@ -1,5 +1,4 @@ export abstract class VaultTimeoutService { abstract checkVaultTimeout(): Promise; abstract lock(userId?: string): Promise; - abstract logOut(userId?: string): Promise; } diff --git a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.spec.ts b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.spec.ts index da815f76f79..5ba434f7188 100644 --- a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.spec.ts +++ b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.spec.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { MockProxy, any, mock } from "jest-mock-extended"; +import { MockProxy, mock } from "jest-mock-extended"; import { BehaviorSubject, from, of } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. @@ -8,7 +8,7 @@ import { BehaviorSubject, from, of } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports -import { LogoutReason } from "@bitwarden/auth/common"; +import { LogoutService } from "@bitwarden/auth/common"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { BiometricsService } from "@bitwarden/key-management"; @@ -53,8 +53,8 @@ describe("VaultTimeoutService", () => { let taskSchedulerService: MockProxy; let logService: MockProxy; let biometricsService: MockProxy; + let logoutService: MockProxy; let lockedCallback: jest.Mock, [userId: string]>; - let loggedOutCallback: jest.Mock, [logoutReason: LogoutReason, userId?: string]>; let vaultTimeoutActionSubject: BehaviorSubject; let availableVaultTimeoutActionsSubject: BehaviorSubject; @@ -80,9 +80,9 @@ describe("VaultTimeoutService", () => { taskSchedulerService = mock(); logService = mock(); biometricsService = mock(); + logoutService = mock(); lockedCallback = jest.fn(); - loggedOutCallback = jest.fn(); vaultTimeoutActionSubject = new BehaviorSubject(VaultTimeoutAction.Lock); @@ -110,7 +110,7 @@ describe("VaultTimeoutService", () => { logService, biometricsService, lockedCallback, - loggedOutCallback, + logoutService, ); }); @@ -213,12 +213,12 @@ describe("VaultTimeoutService", () => { }; const expectUserToHaveLoggedOut = (userId: string) => { - expect(loggedOutCallback).toHaveBeenCalledWith("vaultTimeout", userId); + expect(logoutService.logout).toHaveBeenCalledWith(userId, "vaultTimeout"); }; const expectNoAction = (userId: string) => { expect(lockedCallback).not.toHaveBeenCalledWith(userId); - expect(loggedOutCallback).not.toHaveBeenCalledWith(any(), userId); + expect(logoutService.logout).not.toHaveBeenCalledWith(userId, "vaultTimeout"); }; describe("checkVaultTimeout", () => { diff --git a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts index 8b523498c31..c0fa0423694 100644 --- a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts +++ b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts @@ -7,7 +7,7 @@ import { combineLatest, concatMap, filter, firstValueFrom, map, timeout } from " import { CollectionService } from "@bitwarden/admin-console/common"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports -import { LogoutReason } from "@bitwarden/auth/common"; +import { LogoutService } from "@bitwarden/auth/common"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { BiometricsService } from "@bitwarden/key-management"; @@ -52,10 +52,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { protected logService: LogService, private biometricService: BiometricsService, private lockedCallback: (userId: UserId) => Promise = null, - private loggedOutCallback: ( - logoutReason: LogoutReason, - userId?: string, - ) => Promise = null, + private logoutService: LogoutService, ) { this.taskSchedulerService.registerTaskHandler( ScheduledTaskNames.vaultTimeoutCheckInterval, @@ -123,7 +120,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { ); const supportsLock = availableActions.includes(VaultTimeoutAction.Lock); if (!supportsLock) { - await this.logOut(userId); + await this.logoutService.logout(userId, "vaultTimeout"); } // HACK: Start listening for the transition of the locking user from something to the locked state. @@ -165,12 +162,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { } } - async logOut(userId?: string): Promise { - if (this.loggedOutCallback != null) { - await this.loggedOutCallback("vaultTimeout", userId); - } - } - private async shouldLock( userId: string, lastActive: Date, @@ -214,7 +205,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId), ); timeoutAction === VaultTimeoutAction.LogOut - ? await this.logOut(userId) + ? await this.logoutService.logout(userId, "vaultTimeout") : await this.lock(userId); } } From 93ab65cab9f66a8cadfb975fc09dc49a9425728f Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 21 Oct 2025 18:12:27 +0200 Subject: [PATCH 05/16] Fix codeowners for biometrics v2 (#16962) --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 255ddc08c80..ae5d62a90c2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -180,6 +180,8 @@ libs/common/src/key-management @bitwarden/team-key-management-dev libs/node @bitwarden/team-key-management-dev apps/desktop/desktop_native/core/src/biometric/ @bitwarden/team-key-management-dev +apps/desktop/desktop_native/core/src/biometric_v2/ @bitwarden/team-key-management-dev +apps/desktop/desktop_native/core/src/secure_memory/ @bitwarden/team-key-management-dev apps/desktop/src/services/native-messaging.service.ts @bitwarden/team-key-management-dev apps/browser/src/background/nativeMessaging.background.ts @bitwarden/team-key-management-dev apps/desktop/src/services/biometric-message-handler.service.ts @bitwarden/team-key-management-dev From 1794803debcb87bfb8b447729aeda4c3c044c931 Mon Sep 17 00:00:00 2001 From: Kyle Denney <4227399+kdenney@users.noreply.github.com> Date: Tue, 21 Oct 2025 11:18:15 -0500 Subject: [PATCH 06/16] [PM-24032] upgrade nav button updates (#16933) * [PM-24032] upgrade nav button post-upgrade action * fixing broken stories * added component tests * new behavior for the nav button when self hosted * fix stories --- .../upgrade-nav-button.component.html | 2 +- .../upgrade-nav-button.component.spec.ts | 162 ++++++++++++++++++ .../upgrade-nav-button.component.ts | 40 ++++- .../upgrade-nav-button.stories.ts | 21 +++ 4 files changed, 220 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/app/billing/individual/upgrade/upgrade-nav-button/upgrade-nav-button/upgrade-nav-button.component.spec.ts diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-nav-button/upgrade-nav-button/upgrade-nav-button.component.html b/apps/web/src/app/billing/individual/upgrade/upgrade-nav-button/upgrade-nav-button/upgrade-nav-button.component.html index 115c0be86a2..2ffcd14fab0 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-nav-button/upgrade-nav-button/upgrade-nav-button.component.html +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-nav-button/upgrade-nav-button/upgrade-nav-button.component.html @@ -5,7 +5,7 @@ `, @@ -53,6 +55,8 @@ class MockStateService { }) class MockProductSwitcher {} +// 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: "dynamic-avatar", template: ``, @@ -68,6 +72,8 @@ class MockDynamicAvatar implements Partial { ), ); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() text?: string; diff --git a/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts b/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts index 31689914f13..bf5c2754d4e 100644 --- a/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts +++ b/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts @@ -14,6 +14,8 @@ import { DialogService, NavigationModule } from "@bitwarden/components"; import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module"; import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services"; +// 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: "org-switcher", templateUrl: "org-switcher.component.html", @@ -43,20 +45,28 @@ export class OrgSwitcherComponent { * const smFilter = (org: Organization) => org.canAccessSecretsManager * // */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() filter: (org: Organization) => boolean = () => true; /** * Is `true` if the expanded content is visible */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() open = false; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() openChange = new EventEmitter(); /** * Visibility of the New Organization button */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() hideNewButton = false; diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts index 38e7d12f278..873b306a450 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts @@ -16,6 +16,8 @@ import { ProductSwitcherItem, ProductSwitcherService } from "../shared/product-s import { NavigationProductSwitcherComponent } from "./navigation-switcher.component"; +// 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-upgrade-nav-button", template: "
Upgrade Nav Button
", diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.ts index 8a02fdd7647..ab835b67545 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.ts @@ -3,6 +3,8 @@ import { map, Observable } from "rxjs"; import { ProductSwitcherItem, ProductSwitcherService } from "../shared/product-switcher.service"; +// 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: "navigation-product-switcher", templateUrl: "./navigation-switcher.component.html", diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts index fe2821e3d2c..faf1b796b00 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts @@ -37,6 +37,8 @@ class MockOrganizationService implements Partial { return MockOrganizationService._orgs.asObservable(); } + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() set mockOrgs(orgs: Organization[]) { MockOrganizationService._orgs.next(orgs); @@ -54,6 +56,8 @@ class MockProviderService implements Partial { return MockProviderService._providers.asObservable(); } + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() set mockProviders(providers: Provider[]) { MockProviderService._providers.next(providers); @@ -93,6 +97,8 @@ class MockConfigService implements Partial { } } +// 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: "story-layout", template: ``, @@ -100,6 +106,8 @@ class MockConfigService implements Partial { }) class StoryLayoutComponent {} +// 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: "story-content", template: ``, diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts index 5a6572e15be..0157e7b321a 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts @@ -6,12 +6,16 @@ import { MenuComponent } from "@bitwarden/components"; import { ProductSwitcherService } from "./shared/product-switcher.service"; +// 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: "product-switcher-content", templateUrl: "./product-switcher-content.component.html", standalone: false, }) export class ProductSwitcherContentComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("menu") menu: MenuComponent; diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts index 5dd29815ef2..114d95097a2 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts @@ -5,6 +5,8 @@ import { AfterViewInit, ChangeDetectorRef, Component, Input } from "@angular/cor import { IconButtonType } from "@bitwarden/components/src/icon-button/icon-button.component"; import { ProductSwitcherService } from "./shared/product-switcher.service"; +// 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: "product-switcher", templateUrl: "./product-switcher.component.html", @@ -14,6 +16,8 @@ export class ProductSwitcherComponent implements AfterViewInit { /** * Passed to the product switcher's `bitIconButton` */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() buttonType: IconButtonType = "main"; diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts index 66b6a6fb3cf..4c6af713464 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts @@ -37,6 +37,8 @@ class MockOrganizationService implements Partial { return MockOrganizationService._orgs.asObservable(); } + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() set mockOrgs(orgs: Organization[]) { MockOrganizationService._orgs.next(orgs); @@ -54,6 +56,8 @@ class MockProviderService implements Partial { return MockProviderService._providers.asObservable(); } + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() set mockProviders(providers: Provider[]) { MockProviderService._providers.next(providers); @@ -93,6 +97,8 @@ class MockConfigService implements Partial { } } +// 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: "story-layout", template: ``, @@ -100,6 +106,8 @@ class MockConfigService implements Partial { }) class StoryLayoutComponent {} +// 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: "story-content", template: ``, diff --git a/apps/web/src/app/layouts/toggle-width.component.ts b/apps/web/src/app/layouts/toggle-width.component.ts index 411fc73b175..25b8790bb4a 100644 --- a/apps/web/src/app/layouts/toggle-width.component.ts +++ b/apps/web/src/app/layouts/toggle-width.component.ts @@ -4,6 +4,8 @@ import { Component } from "@angular/core"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { NavigationModule } from "@bitwarden/components"; +// 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-toggle-width", template: `({ alias: "appA11yTitle" }); + readonly title = input.required({ alias: "appA11yTitle" }); constructor(private el: ElementRef) { const originalTitle = this.el.nativeElement.getAttribute("title"); diff --git a/libs/components/src/anon-layout/anon-layout-wrapper.component.ts b/libs/components/src/anon-layout/anon-layout-wrapper.component.ts index a17e11b424c..553da0c541b 100644 --- a/libs/components/src/anon-layout/anon-layout-wrapper.component.ts +++ b/libs/components/src/anon-layout/anon-layout-wrapper.component.ts @@ -46,6 +46,8 @@ export interface AnonLayoutWrapperData { hideBackgroundIllustration?: boolean; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "anon-layout-wrapper.component.html", imports: [AnonLayoutComponent, RouterModule], diff --git a/libs/components/src/anon-layout/anon-layout-wrapper.stories.ts b/libs/components/src/anon-layout/anon-layout-wrapper.stories.ts index 7fc022a5ad9..76fcc8976c7 100644 --- a/libs/components/src/anon-layout/anon-layout-wrapper.stories.ts +++ b/libs/components/src/anon-layout/anon-layout-wrapper.stories.ts @@ -103,6 +103,8 @@ type Story = StoryObj; // Default Example +// 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-default-primary-outlet-example-component", template: "

Primary Outlet Example:
your primary component goes here

", @@ -110,6 +112,8 @@ type Story = StoryObj; }) export class DefaultPrimaryOutletExampleComponent {} +// 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-default-secondary-outlet-example-component", template: "

Secondary Outlet Example:
your secondary component goes here

", @@ -117,6 +121,8 @@ export class DefaultPrimaryOutletExampleComponent {} }) export class DefaultSecondaryOutletExampleComponent {} +// 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-default-env-selector-outlet-example-component", template: "

Env Selector Outlet Example:
your env selector component goes here

", @@ -192,6 +198,8 @@ const changedData: AnonLayoutWrapperData = { pageIcon: RegistrationCheckEmailIcon, }; +// 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-dynamic-content-example-component", template: ` diff --git a/libs/components/src/anon-layout/anon-layout.component.ts b/libs/components/src/anon-layout/anon-layout.component.ts index c58b8d7e164..596a54f8825 100644 --- a/libs/components/src/anon-layout/anon-layout.component.ts +++ b/libs/components/src/anon-layout/anon-layout.component.ts @@ -27,6 +27,8 @@ 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({ selector: "auth-anon-layout", templateUrl: "./anon-layout.component.html", diff --git a/libs/components/src/app/app.component.ts b/libs/components/src/app/app.component.ts index ed8cf595d5e..c2829956219 100644 --- a/libs/components/src/app/app.component.ts +++ b/libs/components/src/app/app.component.ts @@ -1,5 +1,7 @@ import { Component } from "@angular/core"; +// 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-root", template: "", diff --git a/libs/components/src/async-actions/in-forms.stories.ts b/libs/components/src/async-actions/in-forms.stories.ts index af5034c49d5..7a19908cbbf 100644 --- a/libs/components/src/async-actions/in-forms.stories.ts +++ b/libs/components/src/async-actions/in-forms.stories.ts @@ -39,6 +39,8 @@ const template = ` `; +// 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-promise-example", template, @@ -84,6 +86,8 @@ class PromiseExampleComponent { }; } +// 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-observable-example", template, diff --git a/libs/components/src/async-actions/standalone.stories.ts b/libs/components/src/async-actions/standalone.stories.ts index 150bdb8813b..52c1e990c23 100644 --- a/libs/components/src/async-actions/standalone.stories.ts +++ b/libs/components/src/async-actions/standalone.stories.ts @@ -20,6 +20,8 @@ const template = /*html*/ ` `; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template, selector: "app-promise-example", @@ -37,6 +39,8 @@ class PromiseExampleComponent { }; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template, selector: "app-action-resolves-quickly", @@ -55,6 +59,8 @@ class ActionResolvesQuicklyComponent { }; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template, selector: "app-observable-example", @@ -66,6 +72,8 @@ class ObservableExampleComponent { }; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template, selector: "app-rejected-promise-example", diff --git a/libs/components/src/avatar/avatar.component.ts b/libs/components/src/avatar/avatar.component.ts index 8ece033c73d..38f85bd7b1e 100644 --- a/libs/components/src/avatar/avatar.component.ts +++ b/libs/components/src/avatar/avatar.component.ts @@ -19,6 +19,8 @@ const SizeClasses: Record = { * A variance in color across the avatar component is important as it is used in Account Switching as a * visual indicator to recognize which of a personal or work account a user is logged into. */ +// 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-avatar", template: ` diff --git a/libs/components/src/badge-list/badge-list.component.ts b/libs/components/src/badge-list/badge-list.component.ts index b9d9a666261..e3d1403be43 100644 --- a/libs/components/src/badge-list/badge-list.component.ts +++ b/libs/components/src/badge-list/badge-list.component.ts @@ -8,6 +8,8 @@ function transformMaxItems(value: number | undefined) { return value == undefined ? undefined : Math.max(1, value); } +// 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-badge-list", templateUrl: "badge-list.component.html", diff --git a/libs/components/src/badge/badge.component.ts b/libs/components/src/badge/badge.component.ts index 37be13ef15a..8a953b30226 100644 --- a/libs/components/src/badge/badge.component.ts +++ b/libs/components/src/badge/badge.component.ts @@ -55,6 +55,8 @@ const hoverStyles: Record = { * > `NOTE:` The `disabled` state only applies to buttons. * */ +// 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: "span[bitBadge], a[bitBadge], button[bitBadge]", providers: [{ provide: FocusableElement, useExisting: BadgeComponent }], diff --git a/libs/components/src/banner/banner.component.ts b/libs/components/src/banner/banner.component.ts index 632bccae953..f258ed0c48c 100644 --- a/libs/components/src/banner/banner.component.ts +++ b/libs/components/src/banner/banner.component.ts @@ -23,6 +23,8 @@ const defaultIcon: Record = { * - Avoid stacking multiple banners. * - Banners can contain a button or anchor that uses the `bitLink` directive with `linkType="secondary"`. */ +// 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-banner", templateUrl: "./banner.component.html", @@ -40,6 +42,8 @@ export class BannerComponent implements OnInit { readonly useAlertRole = input(true); readonly showClose = input(true); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onClose = new EventEmitter(); ngOnInit(): void { diff --git a/libs/components/src/breadcrumbs/breadcrumb.component.ts b/libs/components/src/breadcrumbs/breadcrumb.component.ts index 783cb2655f7..6c79b449a28 100644 --- a/libs/components/src/breadcrumbs/breadcrumb.component.ts +++ b/libs/components/src/breadcrumbs/breadcrumb.component.ts @@ -1,6 +1,8 @@ import { Component, EventEmitter, Output, TemplateRef, input, viewChild } from "@angular/core"; import { QueryParamsHandling } from "@angular/router"; +// 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-breadcrumb", templateUrl: "./breadcrumb.component.html", @@ -14,6 +16,8 @@ export class BreadcrumbComponent { readonly queryParamsHandling = input(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() click = new EventEmitter(); diff --git a/libs/components/src/breadcrumbs/breadcrumbs.component.ts b/libs/components/src/breadcrumbs/breadcrumbs.component.ts index 3c24f91be99..f0e4c4557c3 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.component.ts +++ b/libs/components/src/breadcrumbs/breadcrumbs.component.ts @@ -15,6 +15,8 @@ import { BreadcrumbComponent } from "./breadcrumb.component"; * Bitwarden uses this component to indicate the user's current location in a set of data organized in * containers (Collections, Folders, or Projects). */ +// 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-breadcrumbs", templateUrl: "./breadcrumbs.component.html", @@ -25,6 +27,8 @@ export class BreadcrumbsComponent { private breadcrumbs: BreadcrumbComponent[] = []; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ContentChildren(BreadcrumbComponent) protected set breadcrumbList(value: QueryList) { this.breadcrumbs = value.toArray(); diff --git a/libs/components/src/breadcrumbs/breadcrumbs.stories.ts b/libs/components/src/breadcrumbs/breadcrumbs.stories.ts index 989b72adbd5..3064ffe3cb4 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.stories.ts +++ b/libs/components/src/breadcrumbs/breadcrumbs.stories.ts @@ -18,6 +18,8 @@ interface Breadcrumb { route: string; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "", }) diff --git a/libs/components/src/button/button.component.spec.ts b/libs/components/src/button/button.component.spec.ts index 1651b6cf12a..745be014a0c 100644 --- a/libs/components/src/button/button.component.spec.ts +++ b/libs/components/src/button/button.component.spec.ts @@ -56,6 +56,8 @@ describe("Button", () => { }); }); +// 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: "test-app", template: ` diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index 47612685c16..350d493f832 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -57,6 +57,8 @@ const buttonStyles: Record = { unstyled: [], }; +// 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: "button[bitButton], a[bitButton]", templateUrl: "button.component.html", @@ -97,7 +99,7 @@ export class ButtonComponent implements ButtonLikeAbstraction { .concat(buttonSizeStyles[this.size() || "default"]); } - protected disabledAttr = computed(() => { + protected readonly disabledAttr = computed(() => { const disabled = this.disabled() != null && this.disabled() !== false; return disabled || this.loading(); }); @@ -110,7 +112,7 @@ export class ButtonComponent implements ButtonLikeAbstraction { * We can't use `disabledAttr` for this, because it returns `true` when `loading` is `true`. * We only want to show disabled styles during loading if `showLoadingStyles` is `true`. */ - protected showDisabledStyles = computed(() => { + protected readonly showDisabledStyles = computed(() => { return this.showLoadingStyle() || (this.disabledAttr() && this.loading() === false); }); @@ -134,11 +136,11 @@ export class ButtonComponent implements ButtonLikeAbstraction { * This pattern of converting a signal to an observable and back to a signal is not * recommended. TODO -- find better way to use debounce with signals (CL-596) */ - protected showLoadingStyle = toSignal( + protected readonly showLoadingStyle = toSignal( toObservable(this.loading).pipe(debounce((isLoading) => interval(isLoading ? 75 : 0))), ); - disabled = model(false); + readonly disabled = model(false); private el = inject(ElementRef); constructor() { diff --git a/libs/components/src/callout/callout.component.ts b/libs/components/src/callout/callout.component.ts index c15bc132035..bda5d4585d7 100644 --- a/libs/components/src/callout/callout.component.ts +++ b/libs/components/src/callout/callout.component.ts @@ -28,6 +28,8 @@ let nextId = 0; * sparingly, as they command a large amount of visual attention. Avoid using more than 1 callout in * the same location. */ +// 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-callout", templateUrl: "callout.component.html", diff --git a/libs/components/src/checkbox/checkbox.component.ts b/libs/components/src/checkbox/checkbox.component.ts index d8df53943f3..61d5bb7251c 100644 --- a/libs/components/src/checkbox/checkbox.component.ts +++ b/libs/components/src/checkbox/checkbox.component.ts @@ -3,6 +3,8 @@ import { NgControl, Validators } from "@angular/forms"; import { BitFormControlAbstraction } from "../form-control"; +// 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: "input[type=checkbox][bitCheckbox]", template: "", diff --git a/libs/components/src/checkbox/checkbox.stories.ts b/libs/components/src/checkbox/checkbox.stories.ts index ee3e4ab402d..6355581b251 100644 --- a/libs/components/src/checkbox/checkbox.stories.ts +++ b/libs/components/src/checkbox/checkbox.stories.ts @@ -28,6 +28,8 @@ const template = /*html*/ ` `; +// 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-example", template, @@ -38,10 +40,14 @@ class ExampleComponent { checkbox: [false, Validators.requiredTrue], }); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() set checked(value: boolean) { this.formObj.patchValue({ checkbox: value }); } + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() set disabled(disable: boolean) { if (disable) { this.formObj.disable(); diff --git a/libs/components/src/chip-select/chip-select.component.ts b/libs/components/src/chip-select/chip-select.component.ts index 14f740f3bbe..78f12c400b5 100644 --- a/libs/components/src/chip-select/chip-select.component.ts +++ b/libs/components/src/chip-select/chip-select.component.ts @@ -35,6 +35,8 @@ export type ChipSelectOption = Option & { /** * `` is a select element that is commonly used to filter items in lists or tables. */ +// 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-chip-select", templateUrl: "chip-select.component.html", @@ -49,6 +51,8 @@ export type ChipSelectOption = Option & { }) export class ChipSelectComponent implements ControlValueAccessor, AfterViewInit { readonly menu = viewChild(MenuComponent); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChildren(MenuItemDirective) menuItems?: QueryList; readonly chipSelectButton = viewChild>("chipSelectButton"); @@ -63,6 +67,8 @@ export class ChipSelectComponent implements ControlValueAccessor, A // TODO: Skipped for signal migration because: // Accessor inputs cannot be migrated as they are too complex. /** The select options to render */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ required: true }) get options(): ChipSelectOption[] { return this._options; @@ -75,6 +81,8 @@ export class ChipSelectComponent implements ControlValueAccessor, A /** Disables the entire chip */ // TODO: Skipped for signal migration because: // Your application code writes to the input. This prevents migration. + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ transform: booleanAttribute }) disabled = false; /** Chip will stretch to full width of its container */ @@ -83,7 +91,7 @@ export class ChipSelectComponent implements ControlValueAccessor, A /** * We have `:focus-within` and `:focus-visible` but no `:focus-visible-within` */ - protected focusVisibleWithin = signal(false); + protected readonly focusVisibleWithin = signal(false); @HostListener("focusin", ["$event.target"]) onFocusIn(target: HTMLElement) { this.focusVisibleWithin.set(target.matches("[data-fvw-target]:focus-visible")); diff --git a/libs/components/src/color-password/color-password.component.ts b/libs/components/src/color-password/color-password.component.ts index 3a35eaab333..bd7f3beb403 100644 --- a/libs/components/src/color-password/color-password.component.ts +++ b/libs/components/src/color-password/color-password.component.ts @@ -9,6 +9,8 @@ type CharacterType = "letter" | "emoji" | "special" | "number"; * the logic for displaying letters as `text-main`, numbers as `primary`, and special symbols as * `danger`. */ +// 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-color-password", template: `@for (character of passwordCharArray(); track $index; let i = $index) { @@ -21,11 +23,11 @@ type CharacterType = "letter" | "emoji" | "special" | "number"; }`, }) export class ColorPasswordComponent { - password = input(""); - showCount = input(false); + readonly password = input(""); + readonly showCount = input(false); // Convert to an array to handle cases that strings have special characters, i.e.: emoji. - passwordCharArray = computed(() => { + readonly passwordCharArray = computed(() => { return Array.from(this.password() ?? ""); }); diff --git a/libs/components/src/container/container.component.ts b/libs/components/src/container/container.component.ts index 9f6a4cbef94..40b19b16598 100644 --- a/libs/components/src/container/container.component.ts +++ b/libs/components/src/container/container.component.ts @@ -3,6 +3,8 @@ import { Component } from "@angular/core"; /** * bit-container is a minimally styled component that limits the max width of its content to the tailwind theme variable '4xl'. '4xl' is equal to the value of 56rem */ +// 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-container", templateUrl: "container.component.html", diff --git a/libs/components/src/copy-click/copy-click.directive.spec.ts b/libs/components/src/copy-click/copy-click.directive.spec.ts index 321a18596e4..ed3640b7d6d 100644 --- a/libs/components/src/copy-click/copy-click.directive.spec.ts +++ b/libs/components/src/copy-click/copy-click.directive.spec.ts @@ -9,6 +9,8 @@ import { ToastService, CopyClickListener, COPY_CLICK_LISTENER } from "../"; import { CopyClickDirective } from "./copy-click.directive"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: ` @@ -25,9 +27,17 @@ import { CopyClickDirective } from "./copy-click.directive"; imports: [CopyClickDirective], }) class TestCopyClickComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("noToast") noToastButton!: ElementRef; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("infoToast") infoToastButton!: ElementRef; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("successToast") successToastButton!: ElementRef; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild("toastWithLabel") toastWithLabelButton!: ElementRef; } diff --git a/libs/components/src/copy-click/copy-click.directive.ts b/libs/components/src/copy-click/copy-click.directive.ts index 3eb075a2b5c..f12880f8781 100644 --- a/libs/components/src/copy-click/copy-click.directive.ts +++ b/libs/components/src/copy-click/copy-click.directive.ts @@ -26,11 +26,11 @@ export const COPY_CLICK_LISTENER = new InjectionToken("CopyCl selector: "[appCopyClick]", }) export class CopyClickDirective { - private _showToast = computed(() => { + private readonly _showToast = computed(() => { return this.showToast() !== undefined; }); - private toastVariant = computed(() => { + private readonly toastVariant = computed(() => { const showToast = this.showToast(); // When the `showToast` is set without a value, an empty string will be passed if (showToast === "" || showToast === undefined) { @@ -68,7 +68,7 @@ export class CopyClickDirective { * * ``` */ - showToast = input(); + readonly showToast = input(); @HostListener("click") onClick() { const valueToCopy = this.valueToCopy(); diff --git a/libs/components/src/dialog/dialog.service.stories.ts b/libs/components/src/dialog/dialog.service.stories.ts index caa7a86a2a8..9bcb704a7fd 100644 --- a/libs/components/src/dialog/dialog.service.stories.ts +++ b/libs/components/src/dialog/dialog.service.stories.ts @@ -21,6 +21,8 @@ interface Animal { animal: string; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: ` @@ -62,6 +64,8 @@ class StoryDialogComponent { } } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: ` @@ -91,6 +95,8 @@ class StoryDialogContentComponent { } } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: ` >("scrollBottom"); + private readonly scrollableBody = viewChild.required(CdkScrollable); + private readonly scrollBottom = viewChild.required>("scrollBottom"); protected dialogRef = inject(DialogRef, { optional: true }); protected bodyHasScrolledFrom = hasScrolledFrom(this.scrollableBody); diff --git a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts index 4c04f0e29c9..eed031d76fb 100644 --- a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts +++ b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts @@ -26,6 +26,8 @@ const DEFAULT_COLOR: Record = { danger: "tw-text-danger", }; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "./simple-configurable-dialog.component.html", imports: [ diff --git a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts index 036ef1177e6..7d1b5974d51 100644 --- a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts +++ b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts @@ -10,6 +10,8 @@ import { CalloutModule } from "../../../callout"; import { I18nMockService } from "../../../utils/i18n-mock.service"; import { DialogModule } from "../../dialog.module"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: ` @for (group of dialogs; track group) { diff --git a/libs/components/src/dialog/simple-dialog/simple-dialog.component.ts b/libs/components/src/dialog/simple-dialog/simple-dialog.component.ts index 85f1bed8cf5..cd44a79c271 100644 --- a/libs/components/src/dialog/simple-dialog/simple-dialog.component.ts +++ b/libs/components/src/dialog/simple-dialog/simple-dialog.component.ts @@ -9,6 +9,8 @@ import { DialogTitleContainerDirective } from "../directives/dialog-title-contai }) export class IconDirective {} +// 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-simple-dialog", templateUrl: "./simple-dialog.component.html", @@ -16,12 +18,14 @@ export class IconDirective {} imports: [DialogTitleContainerDirective, TypographyDirective], }) export class SimpleDialogComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ContentChild(IconDirective) icon!: IconDirective; /** * Optional flag to hide the dialog's center icon. Defaults to false. */ - hideIcon = input(false, { transform: booleanAttribute }); + readonly hideIcon = input(false, { transform: booleanAttribute }); get hasIcon() { return this.icon != null; diff --git a/libs/components/src/dialog/simple-dialog/simple-dialog.service.stories.ts b/libs/components/src/dialog/simple-dialog/simple-dialog.service.stories.ts index ce9f7b46fef..5c94a959f25 100644 --- a/libs/components/src/dialog/simple-dialog/simple-dialog.service.stories.ts +++ b/libs/components/src/dialog/simple-dialog/simple-dialog.service.stories.ts @@ -15,6 +15,8 @@ interface Animal { animal: string; } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: ` @@ -57,6 +59,8 @@ class StoryDialogComponent { } } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: ` @@ -87,6 +91,8 @@ class SimpleDialogContent { } } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: ` @@ -116,6 +122,8 @@ class NonDismissableWithPrimaryButtonContent { } } +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: ` diff --git a/libs/components/src/disclosure/disclosure.component.ts b/libs/components/src/disclosure/disclosure.component.ts index 2d73d7d8ad6..60e886f1dcc 100644 --- a/libs/components/src/disclosure/disclosure.component.ts +++ b/libs/components/src/disclosure/disclosure.component.ts @@ -34,12 +34,16 @@ let nextId = 0; * ``` * */ +// 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-disclosure", template: ``, }) export class DisclosureComponent { /** Emits the visibility of the disclosure content */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() openChange = new EventEmitter(); private _open?: boolean; @@ -48,6 +52,8 @@ export class DisclosureComponent { */ // TODO: Skipped for signal migration because: // Accessor inputs cannot be migrated as they are too complex. + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ transform: booleanAttribute }) set open(isOpen: boolean) { this._open = isOpen; this.openChange.emit(isOpen); diff --git a/libs/components/src/drawer/drawer-header.component.ts b/libs/components/src/drawer/drawer-header.component.ts index 36addcd2bea..006c48e091d 100644 --- a/libs/components/src/drawer/drawer-header.component.ts +++ b/libs/components/src/drawer/drawer-header.component.ts @@ -24,7 +24,7 @@ export class DrawerHeaderComponent { /** * The title to display */ - title = input.required(); + readonly title = input.required(); /** We don't want to set the HTML title attribute with `this.title` */ @HostBinding("attr.title") diff --git a/libs/components/src/drawer/drawer-host.directive.ts b/libs/components/src/drawer/drawer-host.directive.ts index 64eea6a9c06..7804d111ed6 100644 --- a/libs/components/src/drawer/drawer-host.directive.ts +++ b/libs/components/src/drawer/drawer-host.directive.ts @@ -10,7 +10,7 @@ import { Directive, signal } from "@angular/core"; selector: "[bitDrawerHost]", }) export class DrawerHostDirective { - private _portal = signal | undefined>(undefined); + private readonly _portal = signal | undefined>(undefined); /** The portal to display */ portal = this._portal.asReadonly(); diff --git a/libs/components/src/drawer/drawer.component.ts b/libs/components/src/drawer/drawer.component.ts index 7a3c764b16f..042d1eace79 100644 --- a/libs/components/src/drawer/drawer.component.ts +++ b/libs/components/src/drawer/drawer.component.ts @@ -25,7 +25,7 @@ import { DrawerService } from "./drawer.service"; }) export class DrawerComponent { private drawerHost = inject(DrawerService); - private portal = viewChild.required(CdkPortal); + private readonly portal = viewChild.required(CdkPortal); /** * Whether or not the drawer is open. @@ -33,7 +33,7 @@ export class DrawerComponent { * Note: Does not support implicit boolean transform due to Angular limitation. Must be bound explicitly `[open]="true"` instead of just `open`. * https://github.com/angular/angular/issues/55166#issuecomment-2032150999 **/ - open = model(false); + readonly open = model(false); /** * The ARIA role of the drawer. @@ -43,7 +43,7 @@ export class DrawerComponent { * - [navigation](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/navigation_role) * - For drawers that primary contain links to other content. */ - role = input<"complementary" | "navigation">("complementary"); + readonly role = input<"complementary" | "navigation">("complementary"); constructor() { effect( diff --git a/libs/components/src/drawer/drawer.service.ts b/libs/components/src/drawer/drawer.service.ts index dd8575efee8..71b3ff967d7 100644 --- a/libs/components/src/drawer/drawer.service.ts +++ b/libs/components/src/drawer/drawer.service.ts @@ -3,7 +3,7 @@ import { Injectable, signal } from "@angular/core"; @Injectable({ providedIn: "root" }) export class DrawerService { - private _portal = signal | undefined>(undefined); + private readonly _portal = signal | undefined>(undefined); /** The portal to display */ portal = this._portal.asReadonly(); diff --git a/libs/components/src/form-control/form-control.component.ts b/libs/components/src/form-control/form-control.component.ts index be1a071bcab..642d280bdcb 100644 --- a/libs/components/src/form-control/form-control.component.ts +++ b/libs/components/src/form-control/form-control.component.ts @@ -8,6 +8,8 @@ import { TypographyDirective } from "../typography/typography.directive"; import { BitFormControlAbstraction } from "./form-control.abstraction"; +// 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-form-control", templateUrl: "form-control.component.html", diff --git a/libs/components/src/form-control/label.component.ts b/libs/components/src/form-control/label.component.ts index 224e00c31da..57f9e338bb6 100644 --- a/libs/components/src/form-control/label.component.ts +++ b/libs/components/src/form-control/label.component.ts @@ -6,6 +6,8 @@ import { FormControlComponent } from "./form-control.component"; // Increments for each instance of this component let nextId = 0; +// 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-label", templateUrl: "label.component.html", diff --git a/libs/components/src/form-field/error-summary.component.ts b/libs/components/src/form-field/error-summary.component.ts index 1b0b5bb3cbf..cf26fd1e21f 100644 --- a/libs/components/src/form-field/error-summary.component.ts +++ b/libs/components/src/form-field/error-summary.component.ts @@ -3,6 +3,8 @@ import { AbstractControl, UntypedFormGroup } from "@angular/forms"; 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-error-summary", template: ` @if (errorCount > 0) { diff --git a/libs/components/src/form-field/error.component.ts b/libs/components/src/form-field/error.component.ts index ed9d7eafe8d..9d931a64db2 100644 --- a/libs/components/src/form-field/error.component.ts +++ b/libs/components/src/form-field/error.component.ts @@ -5,6 +5,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic // Increments for each instance of this component let nextId = 0; +// 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-error", template: ` {{ displayError }}`, diff --git a/libs/components/src/form-field/form-field.component.ts b/libs/components/src/form-field/form-field.component.ts index 26038caa466..ccfdcec4132 100644 --- a/libs/components/src/form-field/form-field.component.ts +++ b/libs/components/src/form-field/form-field.component.ts @@ -22,6 +22,8 @@ import { inputBorderClasses } from "../input/input.directive"; import { BitErrorComponent } from "./error.component"; import { BitFormFieldControl } from "./form-field-control"; +// 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-form-field", templateUrl: "./form-field.component.html", @@ -42,11 +44,13 @@ export class BitFormFieldComponent implements AfterContentChecked { /** If `true`, remove the bottom border for `readonly` inputs */ // TODO: Skipped for signal migration because: // Your application code writes to the input. This prevents migration. + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ transform: booleanAttribute }) disableReadOnlyBorder = false; - protected prefixHasChildren = signal(false); - protected suffixHasChildren = signal(false); + protected readonly prefixHasChildren = signal(false); + protected readonly suffixHasChildren = signal(false); get inputBorderClasses(): string { const shouldFocusBorderAppear = this.defaultContentIsFocused(); @@ -87,7 +91,7 @@ export class BitFormFieldComponent implements AfterContentChecked { * This is necessary because the `tw-group/bit-form-field` wraps the input and any prefix/suffix * buttons */ - protected defaultContentIsFocused = signal(false); + protected readonly defaultContentIsFocused = signal(false); @HostListener("focusin", ["$event.target"]) onFocusIn(target: HTMLElement) { this.defaultContentIsFocused.set(target.matches("[data-default-content] *:focus-visible")); diff --git a/libs/components/src/form-field/password-input-toggle.directive.ts b/libs/components/src/form-field/password-input-toggle.directive.ts index 251878f5cef..ba5539250a6 100644 --- a/libs/components/src/form-field/password-input-toggle.directive.ts +++ b/libs/components/src/form-field/password-input-toggle.directive.ts @@ -27,6 +27,8 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan * Whether the input is toggled to show the password. */ readonly toggled = model(false); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() toggledChange = new EventEmitter(); @HostBinding("attr.title") title = this.i18nService.t("toggleVisibility"); diff --git a/libs/components/src/form-field/password-input-toggle.spec.ts b/libs/components/src/form-field/password-input-toggle.spec.ts index 2b82fad9876..2a90622513a 100644 --- a/libs/components/src/form-field/password-input-toggle.spec.ts +++ b/libs/components/src/form-field/password-input-toggle.spec.ts @@ -13,6 +13,8 @@ import { BitFormFieldComponent } from "./form-field.component"; import { FormFieldModule } from "./form-field.module"; import { BitPasswordInputToggleDirective } from "./password-input-toggle.directive"; +// 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: "test-form-field", template: ` diff --git a/libs/components/src/icon-button/icon-button.component.ts b/libs/components/src/icon-button/icon-button.component.ts index d712d5cb2b8..f1edee7c089 100644 --- a/libs/components/src/icon-button/icon-button.component.ts +++ b/libs/components/src/icon-button/icon-button.component.ts @@ -81,6 +81,8 @@ const sizes: Record = { * Similar to the main button components, spacing between multiple icon buttons should be .5rem. */ +// 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: "button[bitIconButton]:not(button[bitButton])", templateUrl: "icon-button.component.html", @@ -143,7 +145,7 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE return [this.icon(), "!tw-m-0"]; } - protected disabledAttr = computed(() => { + protected readonly disabledAttr = computed(() => { const disabled = this.disabled() != null && this.disabled() !== false; return disabled || this.loading(); }); @@ -156,7 +158,7 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE * We can't use `disabledAttr` for this, because it returns `true` when `loading` is `true`. * We only want to show disabled styles during loading if `showLoadingStyles` is `true`. */ - protected showDisabledStyles = computed(() => { + protected readonly showDisabledStyles = computed(() => { return this.showLoadingStyle() || (this.disabledAttr() && this.loading() === false); }); @@ -174,7 +176,7 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE * This pattern of converting a signal to an observable and back to a signal is not * recommended. TODO -- find better way to use debounce with signals (CL-596) */ - protected showLoadingStyle = toSignal( + protected readonly showLoadingStyle = toSignal( toObservable(this.loading).pipe(debounce((isLoading) => interval(isLoading ? 75 : 0))), ); diff --git a/libs/components/src/icon-tile/icon-tile.component.ts b/libs/components/src/icon-tile/icon-tile.component.ts index 54e92f9f004..68eed007df0 100644 --- a/libs/components/src/icon-tile/icon-tile.component.ts +++ b/libs/components/src/icon-tile/icon-tile.component.ts @@ -56,6 +56,8 @@ const shapeStyles: Record> = { * - Create visual hierarchy in lists or cards * - Show app or service icons in a consistent format */ +// 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-icon-tile", templateUrl: "icon-tile.component.html", diff --git a/libs/components/src/icon/icon.component.ts b/libs/components/src/icon/icon.component.ts index 1ef7185c88b..f57a3627383 100644 --- a/libs/components/src/icon/icon.component.ts +++ b/libs/components/src/icon/icon.component.ts @@ -3,6 +3,8 @@ import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; import { Icon, isIcon } from "@bitwarden/assets/svg"; +// 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-icon", host: { diff --git a/libs/components/src/input/input.directive.ts b/libs/components/src/input/input.directive.ts index 465736f7baa..ce34b70e88b 100644 --- a/libs/components/src/input/input.directive.ts +++ b/libs/components/src/input/input.directive.ts @@ -74,6 +74,8 @@ export class BitInputDirective implements BitFormFieldControl { // TODO: Skipped for signal migration because: // Accessor inputs cannot be migrated as they are too complex. @HostBinding() + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get required() { return this._required ?? this.ngControl?.control?.hasValidator(Validators.required) ?? false; diff --git a/libs/components/src/item/item-action.component.ts b/libs/components/src/item/item-action.component.ts index acbc805cf90..b7ce1705f7c 100644 --- a/libs/components/src/item/item-action.component.ts +++ b/libs/components/src/item/item-action.component.ts @@ -1,5 +1,7 @@ import { Component } from "@angular/core"; +// 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-item-action", imports: [], diff --git a/libs/components/src/item/item-content.component.ts b/libs/components/src/item/item-content.component.ts index 1f4ac73c77e..9c24d010946 100644 --- a/libs/components/src/item/item-content.component.ts +++ b/libs/components/src/item/item-content.component.ts @@ -29,7 +29,7 @@ import { TypographyModule } from "../typography"; export class ItemContentComponent implements AfterContentChecked { readonly endSlot = viewChild>("endSlot"); - protected endSlotHasChildren = signal(false); + protected readonly endSlotHasChildren = signal(false); /** * Determines whether text will truncate or wrap. diff --git a/libs/components/src/item/item.component.ts b/libs/components/src/item/item.component.ts index f6f2c3d7e35..a3fa8386325 100644 --- a/libs/components/src/item/item.component.ts +++ b/libs/components/src/item/item.component.ts @@ -22,7 +22,7 @@ export class ItemComponent { /** * We have `:focus-within` and `:focus-visible` but no `:focus-visible-within` */ - protected focusVisibleWithin = signal(false); + protected readonly focusVisibleWithin = signal(false); @HostListener("focusin", ["$event.target"]) onFocusIn(target: HTMLElement) { this.focusVisibleWithin.set(target.matches("[data-fvw-target]:focus-visible")); diff --git a/libs/components/src/layout/layout.component.ts b/libs/components/src/layout/layout.component.ts index 54b0341603c..1b357424205 100644 --- a/libs/components/src/layout/layout.component.ts +++ b/libs/components/src/layout/layout.component.ts @@ -12,6 +12,8 @@ import { SharedModule } from "../shared"; import { ScrollLayoutHostDirective } from "./scroll-layout.directive"; +// 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-layout", templateUrl: "layout.component.html", @@ -34,7 +36,7 @@ export class LayoutComponent { protected sideNavService = inject(SideNavService); protected drawerPortal = inject(DrawerService).portal; - private mainContent = viewChild.required>("main"); + private readonly mainContent = viewChild.required>("main"); protected focusMainContent() { this.mainContent().nativeElement.focus(); } @@ -45,7 +47,7 @@ export class LayoutComponent { * * @see https://github.com/angular/components/issues/10247#issuecomment-384060265 **/ - private skipLink = viewChild.required>("skipLink"); + private readonly skipLink = viewChild.required>("skipLink"); handleKeydown(ev: KeyboardEvent) { if (isNothingFocused()) { ev.preventDefault(); diff --git a/libs/components/src/layout/scroll-layout.directive.ts b/libs/components/src/layout/scroll-layout.directive.ts index cb2c2a4e431..35a4c5b63d8 100644 --- a/libs/components/src/layout/scroll-layout.directive.ts +++ b/libs/components/src/layout/scroll-layout.directive.ts @@ -17,7 +17,7 @@ import { filter, fromEvent, Observable, switchMap } from "rxjs"; **/ @Injectable({ providedIn: "root" }) export class ScrollLayoutService { - scrollableRef = signal | null>(null); + readonly scrollableRef = signal | null>(null); scrollableRef$ = toObservable(this.scrollableRef); } diff --git a/libs/components/src/link/link.directive.ts b/libs/components/src/link/link.directive.ts index 7c93b185a79..e6de8ac8402 100644 --- a/libs/components/src/link/link.directive.ts +++ b/libs/components/src/link/link.directive.ts @@ -99,7 +99,7 @@ export class AnchorLinkDirective extends LinkDirective { export class ButtonLinkDirective extends LinkDirective { private el = inject(ElementRef); - disabled = input(false, { transform: booleanAttribute }); + readonly disabled = input(false, { transform: booleanAttribute }); @HostBinding("class") get classList() { return ["before:-tw-inset-y-[0.25rem]"] diff --git a/libs/components/src/menu/menu-divider.component.ts b/libs/components/src/menu/menu-divider.component.ts index 194506ee50f..3bf992d3688 100644 --- a/libs/components/src/menu/menu-divider.component.ts +++ b/libs/components/src/menu/menu-divider.component.ts @@ -1,5 +1,7 @@ import { Component } from "@angular/core"; +// 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-menu-divider", templateUrl: "./menu-divider.component.html", diff --git a/libs/components/src/menu/menu-item.directive.ts b/libs/components/src/menu/menu-item.directive.ts index 088e2a4293f..e19e208f7b0 100644 --- a/libs/components/src/menu/menu-item.directive.ts +++ b/libs/components/src/menu/menu-item.directive.ts @@ -3,6 +3,8 @@ import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { NgClass } from "@angular/common"; import { Component, ElementRef, HostBinding, Input } from "@angular/core"; +// 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: "[bitMenuItem]", templateUrl: "menu-item.component.html", @@ -42,6 +44,8 @@ export class MenuItemDirective implements FocusableOption { // TODO: Skipped for signal migration because: // This input overrides a field from a superclass, while the superclass field // is not migrated. + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ transform: coerceBooleanProperty }) disabled?: boolean = false; constructor(public elementRef: ElementRef) {} diff --git a/libs/components/src/menu/menu.component.spec.ts b/libs/components/src/menu/menu.component.spec.ts index 3153fd4eb37..59ad7c6dbc8 100644 --- a/libs/components/src/menu/menu.component.spec.ts +++ b/libs/components/src/menu/menu.component.spec.ts @@ -68,6 +68,8 @@ describe("Menu", () => { }); }); +// 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: "test-app", template: ` diff --git a/libs/components/src/menu/menu.component.ts b/libs/components/src/menu/menu.component.ts index 3cc4de9f90f..f32a790ef69 100644 --- a/libs/components/src/menu/menu.component.ts +++ b/libs/components/src/menu/menu.component.ts @@ -12,6 +12,8 @@ import { import { MenuItemDirective } from "./menu-item.directive"; +// 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-menu", templateUrl: "./menu.component.html", @@ -20,6 +22,8 @@ import { MenuItemDirective } from "./menu-item.directive"; }) export class MenuComponent implements AfterContentInit { readonly templateRef = viewChild.required(TemplateRef); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() closed = new EventEmitter(); readonly menuItems = contentChildren(MenuItemDirective, { descendants: true }); keyManager?: FocusKeyManager; diff --git a/libs/components/src/multi-select/multi-select.component.ts b/libs/components/src/multi-select/multi-select.component.ts index 917bb7a9e3d..4e755d1deda 100644 --- a/libs/components/src/multi-select/multi-select.component.ts +++ b/libs/components/src/multi-select/multi-select.component.ts @@ -34,6 +34,8 @@ import { SelectItemView } from "./models/select-item-view"; // Increments for each instance of this component let nextId = 0; +// 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-multi-select", templateUrl: "./multi-select.component.html", @@ -64,6 +66,8 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro readonly loading = input(false); // TODO: Skipped for signal migration because: // Your application code writes to the input. This prevents migration. + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ transform: booleanAttribute }) disabled?: boolean; // Internal tracking of selected items @@ -79,6 +83,8 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro /**Implemented as part of NG_VALUE_ACCESSOR */ private notifyOnTouched?: () => void; + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onItemsConfirmed = new EventEmitter(); constructor( @@ -208,6 +214,8 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro // TODO: Skipped for signal migration because: // Accessor inputs cannot be migrated as they are too complex. @HostBinding("attr.required") + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get required() { return this._required ?? this.ngControl?.control?.hasValidator(Validators.required) ?? false; diff --git a/libs/components/src/navigation/nav-base.component.ts b/libs/components/src/navigation/nav-base.component.ts index 1ca40545cbb..9092ac4447c 100644 --- a/libs/components/src/navigation/nav-base.component.ts +++ b/libs/components/src/navigation/nav-base.component.ts @@ -61,5 +61,7 @@ export abstract class NavBaseComponent { /** * Fires when main content is clicked */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() mainContentClicked: EventEmitter = new EventEmitter(); } diff --git a/libs/components/src/navigation/nav-divider.component.ts b/libs/components/src/navigation/nav-divider.component.ts index 52fb433c54d..2f33883fd58 100644 --- a/libs/components/src/navigation/nav-divider.component.ts +++ b/libs/components/src/navigation/nav-divider.component.ts @@ -3,6 +3,8 @@ import { Component } from "@angular/core"; import { SideNavService } from "./side-nav.service"; +// 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-nav-divider", templateUrl: "./nav-divider.component.html", diff --git a/libs/components/src/navigation/nav-group.component.ts b/libs/components/src/navigation/nav-group.component.ts index 1fa03ef52be..e3bb02bb75a 100644 --- a/libs/components/src/navigation/nav-group.component.ts +++ b/libs/components/src/navigation/nav-group.component.ts @@ -19,6 +19,8 @@ import { NavBaseComponent } from "./nav-base.component"; import { NavGroupAbstraction, NavItemComponent } from "./nav-item.component"; import { SideNavService } from "./side-nav.service"; +// 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-nav-group", templateUrl: "./nav-group.component.html", @@ -51,6 +53,8 @@ export class NavGroupComponent extends NavBaseComponent { */ readonly hideIfEmpty = input(false, { transform: booleanAttribute }); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() openChange = new EventEmitter(); diff --git a/libs/components/src/navigation/nav-group.stories.ts b/libs/components/src/navigation/nav-group.stories.ts index 8bfd8007ac0..29ad169bba9 100644 --- a/libs/components/src/navigation/nav-group.stories.ts +++ b/libs/components/src/navigation/nav-group.stories.ts @@ -12,6 +12,8 @@ import { I18nMockService } from "../utils/i18n-mock.service"; import { NavGroupComponent } from "./nav-group.component"; import { NavigationModule } from "./navigation.module"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: "", }) diff --git a/libs/components/src/navigation/nav-item.component.ts b/libs/components/src/navigation/nav-item.component.ts index 8d57a752702..5b5709ebebb 100644 --- a/libs/components/src/navigation/nav-item.component.ts +++ b/libs/components/src/navigation/nav-item.component.ts @@ -13,6 +13,8 @@ export abstract class NavGroupAbstraction { abstract setOpen(open: boolean): void; } +// 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-nav-item", templateUrl: "./nav-item.component.html", diff --git a/libs/components/src/navigation/nav-logo.component.ts b/libs/components/src/navigation/nav-logo.component.ts index 96258740d74..0602e8b753c 100644 --- a/libs/components/src/navigation/nav-logo.component.ts +++ b/libs/components/src/navigation/nav-logo.component.ts @@ -8,6 +8,8 @@ import { BitIconComponent } from "../icon/icon.component"; import { SideNavService } from "./side-nav.service"; +// 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-nav-logo", templateUrl: "./nav-logo.component.html", diff --git a/libs/components/src/navigation/side-nav.component.ts b/libs/components/src/navigation/side-nav.component.ts index fe428c7011f..b373a89d47e 100644 --- a/libs/components/src/navigation/side-nav.component.ts +++ b/libs/components/src/navigation/side-nav.component.ts @@ -11,6 +11,8 @@ import { SideNavService } from "./side-nav.service"; export type SideNavVariant = "primary" | "secondary"; +// 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-side-nav", templateUrl: "side-nav.component.html", diff --git a/libs/components/src/no-items/no-items.component.ts b/libs/components/src/no-items/no-items.component.ts index 517abfc9533..c6e52a1f83d 100644 --- a/libs/components/src/no-items/no-items.component.ts +++ b/libs/components/src/no-items/no-items.component.ts @@ -7,6 +7,8 @@ import { BitIconComponent } from "../icon/icon.component"; /** * Component for displaying a message when there are no items to display. Expects title, description and button slots. */ +// 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-no-items", templateUrl: "./no-items.component.html", diff --git a/libs/components/src/popover/popover.component.ts b/libs/components/src/popover/popover.component.ts index 78bc2abe369..024a293ada3 100644 --- a/libs/components/src/popover/popover.component.ts +++ b/libs/components/src/popover/popover.component.ts @@ -5,6 +5,8 @@ import { IconButtonModule } from "../icon-button/icon-button.module"; import { SharedModule } from "../shared/shared.module"; import { TypographyModule } from "../typography"; +// 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-popover", imports: [A11yModule, IconButtonModule, SharedModule, TypographyModule], @@ -14,5 +16,7 @@ import { TypographyModule } from "../typography"; export class PopoverComponent { readonly templateRef = viewChild.required(TemplateRef); readonly title = input(""); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() closed = new EventEmitter(); } diff --git a/libs/components/src/progress/progress.component.ts b/libs/components/src/progress/progress.component.ts index cd1d55ac9eb..c04b7c90609 100644 --- a/libs/components/src/progress/progress.component.ts +++ b/libs/components/src/progress/progress.component.ts @@ -20,6 +20,8 @@ const BackgroundClasses: Record = { /** * Progress indicators may be used to visually indicate progress or to visually measure some other value, such as a password strength indicator. */ +// 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-progress", templateUrl: "./progress.component.html", diff --git a/libs/components/src/radio-button/radio-button.component.spec.ts b/libs/components/src/radio-button/radio-button.component.spec.ts index b47c1823160..3ce5ebae65e 100644 --- a/libs/components/src/radio-button/radio-button.component.spec.ts +++ b/libs/components/src/radio-button/radio-button.component.spec.ts @@ -53,6 +53,8 @@ describe("RadioButton", () => { }); }); +// 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: "test-component", template: ` diff --git a/libs/components/src/radio-button/radio-button.component.ts b/libs/components/src/radio-button/radio-button.component.ts index 84ed240b3f8..a004e7e07e2 100644 --- a/libs/components/src/radio-button/radio-button.component.ts +++ b/libs/components/src/radio-button/radio-button.component.ts @@ -7,6 +7,8 @@ import { RadioInputComponent } from "./radio-input.component"; let nextId = 0; +// 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-radio-button", templateUrl: "radio-button.component.html", diff --git a/libs/components/src/radio-button/radio-group.component.spec.ts b/libs/components/src/radio-button/radio-group.component.spec.ts index ff01b9323f7..94cc7bc25cb 100644 --- a/libs/components/src/radio-button/radio-group.component.spec.ts +++ b/libs/components/src/radio-button/radio-group.component.spec.ts @@ -63,6 +63,8 @@ describe("RadioGroupComponent", () => { }); }); +// 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: "test-app", template: ` diff --git a/libs/components/src/radio-button/radio-group.component.ts b/libs/components/src/radio-button/radio-group.component.ts index baa2cd58d80..dd44623b313 100644 --- a/libs/components/src/radio-button/radio-group.component.ts +++ b/libs/components/src/radio-button/radio-group.component.ts @@ -8,6 +8,8 @@ import { BitLabel } from "../form-control/label.component"; let nextId = 0; +// 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-radio-group", templateUrl: "radio-group.component.html", diff --git a/libs/components/src/radio-button/radio-input.component.ts b/libs/components/src/radio-button/radio-input.component.ts index e32dc5c572d..47bb799d88e 100644 --- a/libs/components/src/radio-button/radio-input.component.ts +++ b/libs/components/src/radio-button/radio-input.component.ts @@ -5,6 +5,8 @@ import { BitFormControlAbstraction } from "../form-control"; let nextId = 0; +// 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: "input[type=radio][bitRadio]", template: "", diff --git a/libs/components/src/resize-observer/resize-observer.directive.ts b/libs/components/src/resize-observer/resize-observer.directive.ts index 5943636f450..68b859c3191 100644 --- a/libs/components/src/resize-observer/resize-observer.directive.ts +++ b/libs/components/src/resize-observer/resize-observer.directive.ts @@ -13,6 +13,8 @@ export class ResizeObserverDirective implements OnDestroy { } }); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() resize = new EventEmitter(); diff --git a/libs/components/src/search/search.component.ts b/libs/components/src/search/search.component.ts index 65568b241fb..576031d50a7 100644 --- a/libs/components/src/search/search.component.ts +++ b/libs/components/src/search/search.component.ts @@ -18,6 +18,8 @@ let nextId = 0; /** * Do not nest Search components inside another `
`, as they already contain their own standalone `` element for searching. */ +// 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-search", templateUrl: "./search.component.html", @@ -45,10 +47,12 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement { // Use `type="text"` for Safari to improve rendering performance protected inputType = isBrowserSafariApi() ? ("text" as const) : ("search" as const); - protected isInputFocused = signal(false); - protected isFormHovered = signal(false); + protected readonly isInputFocused = signal(false); + protected readonly isFormHovered = signal(false); - protected showResetButton = computed(() => this.isInputFocused() || this.isFormHovered()); + protected readonly showResetButton = computed( + () => this.isInputFocused() || this.isFormHovered(), + ); readonly disabled = model(); readonly placeholder = input(); diff --git a/libs/components/src/section/section-header.component.ts b/libs/components/src/section/section-header.component.ts index c96f9486b52..27577590f41 100644 --- a/libs/components/src/section/section-header.component.ts +++ b/libs/components/src/section/section-header.component.ts @@ -2,6 +2,8 @@ import { Component } from "@angular/core"; import { TypographyModule } from "../typography"; +// 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-section-header", templateUrl: "./section-header.component.html", diff --git a/libs/components/src/section/section.component.ts b/libs/components/src/section/section.component.ts index f65c492a0c1..1ad80610637 100644 --- a/libs/components/src/section/section.component.ts +++ b/libs/components/src/section/section.component.ts @@ -2,6 +2,8 @@ import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { CommonModule } from "@angular/common"; import { Component, input } from "@angular/core"; +// 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-section", imports: [CommonModule], diff --git a/libs/components/src/select/option.component.ts b/libs/components/src/select/option.component.ts index ae75fe6514d..b981d0e0bdc 100644 --- a/libs/components/src/select/option.component.ts +++ b/libs/components/src/select/option.component.ts @@ -2,6 +2,8 @@ import { Component, booleanAttribute, input } from "@angular/core"; import { MappedOptionComponent } from "./option"; +// 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-option", template: ``, diff --git a/libs/components/src/select/select.component.spec.ts b/libs/components/src/select/select.component.spec.ts index cfc69636224..21ae50611a7 100644 --- a/libs/components/src/select/select.component.spec.ts +++ b/libs/components/src/select/select.component.spec.ts @@ -9,6 +9,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { SelectComponent } from "./select.component"; import { SelectModule } from "./select.module"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ imports: [SelectModule, ReactiveFormsModule], template: ` diff --git a/libs/components/src/select/select.component.ts b/libs/components/src/select/select.component.ts index 90b39e9c13a..f6358ccf6c6 100644 --- a/libs/components/src/select/select.component.ts +++ b/libs/components/src/select/select.component.ts @@ -34,6 +34,8 @@ import { OptionComponent } from "./option.component"; let nextId = 0; +// 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-select", templateUrl: "select.component.html", @@ -50,10 +52,12 @@ export class SelectComponent implements BitFormFieldControl, ControlValueAcce readonly items = model[] | undefined>(); readonly placeholder = input(this.i18nService.t("selectPlaceholder")); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() closed = new EventEmitter(); - protected selectedValue = signal(undefined); - selectedOption: Signal | null | undefined> = computed(() => + protected readonly selectedValue = signal(undefined); + readonly selectedOption: Signal | null | undefined> = computed(() => this.findSelectedOption(this.items(), this.selectedValue()), ); protected searchInputId = `bit-select-search-input-${nextId++}`; @@ -70,6 +74,8 @@ export class SelectComponent implements BitFormFieldControl, ControlValueAcce } } + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ContentChildren(OptionComponent) protected set options(value: QueryList>) { if (value == null || value.length == 0) { @@ -94,6 +100,8 @@ export class SelectComponent implements BitFormFieldControl, ControlValueAcce } // TODO: Skipped for signal migration because: // Accessor inputs cannot be migrated as they are too complex. + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get disabled() { return this._disabled ?? this.ngControl?.disabled ?? false; @@ -166,6 +174,8 @@ export class SelectComponent implements BitFormFieldControl, ControlValueAcce // TODO: Skipped for signal migration because: // Accessor inputs cannot be migrated as they are too complex. @HostBinding("attr.required") + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get required() { return this._required ?? this.ngControl?.control?.hasValidator(Validators.required) ?? false; diff --git a/libs/components/src/skeleton/skeleton-group.component.ts b/libs/components/src/skeleton/skeleton-group.component.ts index 8895397ae89..c1d236eafb9 100644 --- a/libs/components/src/skeleton/skeleton-group.component.ts +++ b/libs/components/src/skeleton/skeleton-group.component.ts @@ -7,6 +7,8 @@ import { Component } from "@angular/core"; * Pass skeleton loaders into the start, default, and end content slots. The content within each slot * is fully customizable. */ +// 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-skeleton-group", templateUrl: "./skeleton-group.component.html", diff --git a/libs/components/src/skeleton/skeleton-text.component.ts b/libs/components/src/skeleton/skeleton-text.component.ts index 1bc88bd3204..a3d2c4a8451 100644 --- a/libs/components/src/skeleton/skeleton-text.component.ts +++ b/libs/components/src/skeleton/skeleton-text.component.ts @@ -10,6 +10,8 @@ import { SkeletonComponent } from "./skeleton.component"; * Customize the number of lines represented with the `lines` input. Customize the width * by applying a class to the `bit-skeleton-text` element (i.e. `tw-w-1/2`). */ +// 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-skeleton-text", templateUrl: "./skeleton-text.component.html", @@ -27,5 +29,5 @@ export class SkeletonTextComponent { /** * Array-transformed version of the `lines` to loop over */ - protected linesArray = computed(() => [...Array(this.lines()).keys()]); + protected readonly linesArray = computed(() => [...Array(this.lines()).keys()]); } diff --git a/libs/components/src/skeleton/skeleton.component.ts b/libs/components/src/skeleton/skeleton.component.ts index a9d83dd80a5..56de186c2e4 100644 --- a/libs/components/src/skeleton/skeleton.component.ts +++ b/libs/components/src/skeleton/skeleton.component.ts @@ -10,6 +10,8 @@ import { Component, input } from "@angular/core"; * * If you're looking to represent lines of text, use the `bit-skeleton-text` helper component. */ +// 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-skeleton", templateUrl: "./skeleton.component.html", diff --git a/libs/components/src/spinner/spinner.component.ts b/libs/components/src/spinner/spinner.component.ts index c18e95d70fa..d0c2b90a165 100644 --- a/libs/components/src/spinner/spinner.component.ts +++ b/libs/components/src/spinner/spinner.component.ts @@ -3,6 +3,8 @@ import { Component, HostBinding, Input, booleanAttribute } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +// 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-spinner", templateUrl: "spinner.component.html", @@ -13,21 +15,29 @@ export class SpinnerComponent { /** * The size of the spinner. Defaults to `large`. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() size: "fill" | "small" | "large" = "large"; /** * Disable the default color of the spinner, inherits the text color. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ transform: booleanAttribute }) noColor = false; /** * Accessibility title. Defaults to `Loading`. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() title = this.i18nService.t("loading"); /** * Display text for screen readers. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input({ transform: booleanAttribute }) sr = true; @HostBinding("class") get classList() { diff --git a/libs/components/src/stepper/step.component.ts b/libs/components/src/stepper/step.component.ts index 6d558964d89..2d42dcb23cb 100644 --- a/libs/components/src/stepper/step.component.ts +++ b/libs/components/src/stepper/step.component.ts @@ -1,6 +1,8 @@ import { CdkStep, CdkStepper } from "@angular/cdk/stepper"; import { Component, input } from "@angular/core"; +// 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-step", templateUrl: "step.component.html", @@ -8,7 +10,7 @@ import { Component, input } from "@angular/core"; standalone: true, }) export class StepComponent extends CdkStep { - subLabel = input(); + readonly subLabel = input(); constructor(stepper: CdkStepper) { super(stepper); diff --git a/libs/components/src/stepper/stepper.component.ts b/libs/components/src/stepper/stepper.component.ts index befd4b40cf3..abef0dc3f9a 100644 --- a/libs/components/src/stepper/stepper.component.ts +++ b/libs/components/src/stepper/stepper.component.ts @@ -12,6 +12,8 @@ import { StepComponent } from "./step.component"; * The `` component extends the * [Angular CdkStepper](https://material.angular.io/cdk/stepper/api#CdkStepper) component */ +// 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-stepper", templateUrl: "stepper.component.html", @@ -44,6 +46,8 @@ export class StepperComponent extends CdkStepper { // overriding CdkStepper orientation input so we can default to vertical // TODO: Skipped for signal migration because: // Accessor inputs cannot be migrated as they are too complex. + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() override get orientation() { return this.internalOrientation || "vertical"; diff --git a/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts b/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts index 7f6a6c42f32..60353a28fb6 100644 --- a/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts @@ -7,6 +7,8 @@ import { ScrollLayoutDirective } from "../../../layout"; import { SectionComponent } from "../../../section"; import { TableDataSource, TableModule } from "../../../table"; +// 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: "dialog-virtual-scroll-block", standalone: true, diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts index eb9f2fccac9..8f49415e0dd 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts @@ -7,6 +7,8 @@ import { DialogService } from "../../../dialog"; import { I18nMockService } from "../../../utils/i18n-mock.service"; import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; +// 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-kitchen-sink-form", imports: [KitchenSinkSharedModule], diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts index a96964db8d5..f96217ffb63 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts @@ -8,6 +8,8 @@ import { KitchenSinkForm } from "./kitchen-sink-form.component"; import { KitchenSinkTable } from "./kitchen-sink-table.component"; import { KitchenSinkToggleList } from "./kitchen-sink-toggle-list.component"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ imports: [KitchenSinkSharedModule], template: ` @@ -85,6 +87,8 @@ class KitchenSinkDialog { constructor(public dialogRef: DialogRef) {} } +// 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-tab-main", imports: [KitchenSinkSharedModule, KitchenSinkTable, KitchenSinkToggleList, KitchenSinkForm], @@ -175,7 +179,7 @@ class KitchenSinkDialog { export class KitchenSinkMainComponent { constructor(public dialogService: DialogService) {} - protected drawerOpen = signal(false); + protected readonly drawerOpen = signal(false); openDialog() { this.dialogService.open(KitchenSinkDialog); diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts index 302d9f6c0a8..d8d2e20f9ae 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts @@ -2,6 +2,8 @@ import { Component } from "@angular/core"; import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; +// 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-kitchen-sink-table", imports: [KitchenSinkSharedModule], diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts index ec8787af1bd..18846ce2831 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts @@ -2,6 +2,8 @@ import { Component } from "@angular/core"; import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; +// 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-kitchen-sink-toggle-list", imports: [KitchenSinkSharedModule], diff --git a/libs/components/src/switch/switch.component.spec.ts b/libs/components/src/switch/switch.component.spec.ts index 4d24dbcd150..241035501fa 100644 --- a/libs/components/src/switch/switch.component.spec.ts +++ b/libs/components/src/switch/switch.component.spec.ts @@ -13,6 +13,8 @@ describe("SwitchComponent", () => { let switchComponent: SwitchComponent; let inputEl: HTMLInputElement; + // TODO: Fix this the next time the file is edited. + // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "test-host", imports: [FormsModule, BitLabel, ReactiveFormsModule, SwitchModule], @@ -70,6 +72,8 @@ describe("SwitchComponent", () => { }); it("should update checked when selected input changes outside of a form", async () => { + // TODO: Fix this the next time the file is edited. + // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "test-selected-host", template: `Element`, diff --git a/libs/components/src/switch/switch.component.ts b/libs/components/src/switch/switch.component.ts index f08ed9f46ef..52b726ac353 100644 --- a/libs/components/src/switch/switch.component.ts +++ b/libs/components/src/switch/switch.component.ts @@ -21,6 +21,8 @@ let nextId = 0; /** * Switch component for toggling between two states. Switch actions are meant to take place immediately and are not to be used in a form where saving/submiting actions are required. */ +// 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-switch", providers: [ @@ -46,19 +48,19 @@ export class SwitchComponent implements ControlValueAccessor, AfterViewInit { /** * Model signal for selected state binding when used outside of a form */ - protected selected = model(false); + protected readonly selected = model(false); /** * Model signal for disabled binding when used outside of a form */ - protected disabled = model(false); - protected disabledReasonText = input(null); + protected readonly disabled = model(false); + protected readonly disabledReasonText = input(null); - private hintComponent = contentChild(BitHintComponent); + private readonly hintComponent = contentChild(BitHintComponent); private disabledReasonTextId = `bit-switch-disabled-text-${nextId++}`; - private describedByIds = computed(() => { + private readonly describedByIds = computed(() => { const ids: string[] = []; if (this.disabledReasonText() && this.disabled()) { diff --git a/libs/components/src/table/sortable.component.ts b/libs/components/src/table/sortable.component.ts index 6d5f72dd379..b46c1ee9fbd 100644 --- a/libs/components/src/table/sortable.component.ts +++ b/libs/components/src/table/sortable.component.ts @@ -5,6 +5,8 @@ import { Component, HostBinding, OnInit, input } from "@angular/core"; import type { SortDirection, SortFn } from "./table-data-source"; import { TableComponent } from "./table.component"; +// 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: "th[bitSortable]", template: ` diff --git a/libs/components/src/table/table-scroll.component.ts b/libs/components/src/table/table-scroll.component.ts index 2f9436baa0b..fcdd401a1a2 100644 --- a/libs/components/src/table/table-scroll.component.ts +++ b/libs/components/src/table/table-scroll.component.ts @@ -44,6 +44,8 @@ export class BitRowDef { * * Utilizes virtual scrolling to render large datasets. */ +// 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-table-scroll", templateUrl: "./table-scroll.component.html", diff --git a/libs/components/src/table/table.component.ts b/libs/components/src/table/table.component.ts index 7fdd0c40e91..fff36472a14 100644 --- a/libs/components/src/table/table.component.ts +++ b/libs/components/src/table/table.component.ts @@ -21,6 +21,8 @@ export class TableBodyDirective { constructor(public readonly template: TemplateRef) {} } +// 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-table", templateUrl: "./table.component.html", diff --git a/libs/components/src/tabs/shared/tab-header.component.ts b/libs/components/src/tabs/shared/tab-header.component.ts index 9a35e5a846f..aa0272aa917 100644 --- a/libs/components/src/tabs/shared/tab-header.component.ts +++ b/libs/components/src/tabs/shared/tab-header.component.ts @@ -3,6 +3,8 @@ import { Component } from "@angular/core"; /** * Component used for styling the tab header/background for both content and navigation tabs */ +// 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-tab-header", host: { diff --git a/libs/components/src/tabs/shared/tab-list-item.directive.ts b/libs/components/src/tabs/shared/tab-list-item.directive.ts index c8b96141ecd..bc70fdf6e4b 100644 --- a/libs/components/src/tabs/shared/tab-list-item.directive.ts +++ b/libs/components/src/tabs/shared/tab-list-item.directive.ts @@ -13,6 +13,8 @@ export class TabListItemDirective implements FocusableOption { // TODO: Skipped for signal migration because: // This input overrides a field from a superclass, while the superclass field // is not migrated. + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() disabled = false; @HostBinding("attr.disabled") diff --git a/libs/components/src/tabs/tab-group/tab-body.component.ts b/libs/components/src/tabs/tab-group/tab-body.component.ts index 2dec4ca180d..7382c8576c3 100644 --- a/libs/components/src/tabs/tab-group/tab-body.component.ts +++ b/libs/components/src/tabs/tab-group/tab-body.component.ts @@ -1,6 +1,8 @@ import { TemplatePortal, CdkPortalOutlet } from "@angular/cdk/portal"; import { Component, effect, HostBinding, input } from "@angular/core"; +// 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-tab-body", templateUrl: "tab-body.component.html", diff --git a/libs/components/src/tabs/tab-group/tab-group.component.ts b/libs/components/src/tabs/tab-group/tab-group.component.ts index aad60d876ec..cd9d876945c 100644 --- a/libs/components/src/tabs/tab-group/tab-group.component.ts +++ b/libs/components/src/tabs/tab-group/tab-group.component.ts @@ -27,6 +27,8 @@ import { TabComponent } from "./tab.component"; /** Used to generate unique ID's for each tab component */ let nextId = 0; +// 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-tab-group", templateUrl: "./tab-group.component.html", @@ -57,14 +59,16 @@ export class TabGroupComponent implements AfterContentChecked, AfterViewInit { readonly preserveContent = input(false); /** Error if no `TabComponent` is supplied. (`contentChildren`, used to query for all the tabs, doesn't support `required`) */ - private _tab = contentChild.required(TabComponent); + private readonly _tab = contentChild.required(TabComponent); - protected tabs = contentChildren(TabComponent); + protected readonly tabs = contentChildren(TabComponent); readonly tabLabels = viewChildren(TabListItemDirective); /** The index of the active tab. */ // TODO: Skipped for signal migration because: // Accessor inputs cannot be migrated as they are too complex. + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() get selectedIndex(): number | null { return this._selectedIndex; @@ -75,9 +79,13 @@ export class TabGroupComponent implements AfterContentChecked, AfterViewInit { private _selectedIndex: number | null = null; /** Output to enable support for two-way binding on `[(selectedIndex)]` */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() readonly selectedIndexChange: EventEmitter = new EventEmitter(); /** Event emitted when the tab selection has changed. */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() readonly selectedTabChange: EventEmitter = new EventEmitter(); diff --git a/libs/components/src/tabs/tab-group/tab.component.ts b/libs/components/src/tabs/tab-group/tab.component.ts index 2eb63cce29d..c021f1aff20 100644 --- a/libs/components/src/tabs/tab-group/tab.component.ts +++ b/libs/components/src/tabs/tab-group/tab.component.ts @@ -11,6 +11,8 @@ import { import { TabLabelDirective } from "./tab-label.directive"; +// 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-tab", templateUrl: "./tab.component.html", @@ -33,6 +35,8 @@ export class TabComponent implements OnInit { readonly contentTabIndex = input(); readonly implicitContent = viewChild.required(TemplateRef); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @ContentChild(TabLabelDirective) templateLabel?: TabLabelDirective; private _contentPortal: TemplatePortal | null = null; diff --git a/libs/components/src/tabs/tab-nav-bar/tab-link.component.ts b/libs/components/src/tabs/tab-nav-bar/tab-link.component.ts index 402c05bfef7..e94118f5f4b 100644 --- a/libs/components/src/tabs/tab-nav-bar/tab-link.component.ts +++ b/libs/components/src/tabs/tab-nav-bar/tab-link.component.ts @@ -16,6 +16,8 @@ import { TabListItemDirective } from "../shared/tab-list-item.directive"; import { TabNavBarComponent } from "./tab-nav-bar.component"; +// 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-tab-link", templateUrl: "tab-link.component.html", @@ -38,6 +40,8 @@ export class TabLinkComponent implements FocusableOption, AfterViewInit { // TODO: Skipped for signal migration because: // This input overrides a field from a superclass, while the superclass field // is not migrated. + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() disabled = false; @HostListener("keydown", ["$event"]) onKeyDown(event: KeyboardEvent) { diff --git a/libs/components/src/tabs/tab-nav-bar/tab-nav-bar.component.ts b/libs/components/src/tabs/tab-nav-bar/tab-nav-bar.component.ts index 26082b1f17e..c7eec302125 100644 --- a/libs/components/src/tabs/tab-nav-bar/tab-nav-bar.component.ts +++ b/libs/components/src/tabs/tab-nav-bar/tab-nav-bar.component.ts @@ -6,6 +6,8 @@ import { TabListContainerDirective } from "../shared/tab-list-container.directiv import { TabLinkComponent } from "./tab-link.component"; +// 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-tab-nav-bar", templateUrl: "tab-nav-bar.component.html", diff --git a/libs/components/src/tabs/tabs.stories.ts b/libs/components/src/tabs/tabs.stories.ts index 1f3f5e353b9..e822e4c72eb 100644 --- a/libs/components/src/tabs/tabs.stories.ts +++ b/libs/components/src/tabs/tabs.stories.ts @@ -12,30 +12,40 @@ import { I18nMockService } from "../utils"; import { TabGroupComponent } from "./tab-group/tab-group.component"; import { TabsModule } from "./tabs.module"; +// 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-tab-active-dummy", template: "Router - Active selected", }) class ActiveDummyComponent {} +// 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-tab-item-2-dummy", template: "Router - Item 2 selected", }) class ItemTwoDummyComponent {} +// 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-tab-item-3-dummy", template: "Router - Item 3 selected", }) class ItemThreeDummyComponent {} +// 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-tab-disabled-dummy", template: "Router - Disabled selected", }) class DisabledDummyComponent {} +// 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-tab-item-with-child-counter-dummy", template: "Router - Item With Child Counter selected", diff --git a/libs/components/src/toast/toast-container.component.ts b/libs/components/src/toast/toast-container.component.ts index 128cf82d355..e455428db9f 100644 --- a/libs/components/src/toast/toast-container.component.ts +++ b/libs/components/src/toast/toast-container.component.ts @@ -1,6 +1,8 @@ import { Component, OnInit, viewChild } from "@angular/core"; import { ToastContainerDirective, ToastrService } from "ngx-toastr"; +// 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-toast-container", templateUrl: "toast-container.component.html", diff --git a/libs/components/src/toast/toast.component.ts b/libs/components/src/toast/toast.component.ts index 1aeca24d0e2..2bc0feebb72 100644 --- a/libs/components/src/toast/toast.component.ts +++ b/libs/components/src/toast/toast.component.ts @@ -25,6 +25,8 @@ const variants: Record = { }, }; +// 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-toast", templateUrl: "toast.component.html", @@ -49,6 +51,8 @@ export class ToastComponent { readonly progressWidth = input(0); /** Emits when the user presses the close button */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onClose = new EventEmitter(); protected get iconClass(): string { diff --git a/libs/components/src/toast/toast.stories.ts b/libs/components/src/toast/toast.stories.ts index 89c8bffad5c..f7eeadce971 100644 --- a/libs/components/src/toast/toast.stories.ts +++ b/libs/components/src/toast/toast.stories.ts @@ -17,12 +17,16 @@ import { ToastOptions, ToastService } from "./toast.service"; const toastServiceExampleTemplate = ` `; +// 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: "toast-service-example", template: toastServiceExampleTemplate, imports: [ButtonModule], }) export class ToastServiceExampleComponent { + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-signals @Input() toastOptions?: ToastOptions; diff --git a/libs/components/src/toast/toastr.component.ts b/libs/components/src/toast/toastr.component.ts index 3b7665f1d64..5f96cb9128d 100644 --- a/libs/components/src/toast/toastr.component.ts +++ b/libs/components/src/toast/toastr.component.ts @@ -7,6 +7,8 @@ import { ToastComponent } from "./toast.component"; /** * Toasts are ephemeral notifications. They most often communicate the result of a user action. Due to their ephemeral nature, long messages and critical alerts should not utilize toasts. */ +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ template: ` { }); }); +// 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: "test-app", template: ` diff --git a/libs/components/src/toggle-group/toggle-group.component.ts b/libs/components/src/toggle-group/toggle-group.component.ts index 16c4063f9e2..bde8f0ea9b6 100644 --- a/libs/components/src/toggle-group/toggle-group.component.ts +++ b/libs/components/src/toggle-group/toggle-group.component.ts @@ -10,6 +10,8 @@ import { let nextId = 0; +// 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-toggle-group", templateUrl: "./toggle-group.component.html", @@ -20,6 +22,8 @@ export class ToggleGroupComponent { readonly fullWidth = input(undefined, { transform: booleanAttribute }); readonly selected = model(); + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() selectedChange = new EventEmitter(); @HostBinding("attr.role") role = "radiogroup"; diff --git a/libs/components/src/toggle-group/toggle.component.spec.ts b/libs/components/src/toggle-group/toggle.component.spec.ts index 1eec7e54e38..d3bc2e0eeed 100644 --- a/libs/components/src/toggle-group/toggle.component.spec.ts +++ b/libs/components/src/toggle-group/toggle.component.spec.ts @@ -45,6 +45,8 @@ describe("Toggle", () => { }); }); +// 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: "test-component", template: ` diff --git a/libs/components/src/toggle-group/toggle.component.ts b/libs/components/src/toggle-group/toggle.component.ts index 0301fe17979..62e886ca572 100644 --- a/libs/components/src/toggle-group/toggle.component.ts +++ b/libs/components/src/toggle-group/toggle.component.ts @@ -14,6 +14,8 @@ import { ToggleGroupComponent } from "./toggle-group.component"; let nextId = 0; +// 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-toggle", templateUrl: "./toggle.component.html", @@ -31,8 +33,8 @@ export class ToggleComponent implements AfterContentChecked, AfterViewIn @HostBinding("tabIndex") tabIndex = "-1"; @HostBinding("class") classList = ["tw-group/toggle", "tw-flex", "tw-min-w-16"]; - protected bitBadgeContainerHasChidlren = signal(false); - protected labelTitle = signal(null); + protected readonly bitBadgeContainerHasChidlren = signal(false); + protected readonly labelTitle = signal(null); get name() { return this.groupComponent.name; diff --git a/libs/components/src/tooltip/tooltip.component.ts b/libs/components/src/tooltip/tooltip.component.ts index 6b240507311..34c67015004 100644 --- a/libs/components/src/tooltip/tooltip.component.ts +++ b/libs/components/src/tooltip/tooltip.component.ts @@ -22,6 +22,8 @@ export const TOOLTIP_DATA = new InjectionToken("TOOLTIP_DATA"); /** * tooltip component used internally by the tooltip.directive. Not meant to be used explicitly */ +// 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-tooltip", templateUrl: "./tooltip.component.html", diff --git a/libs/components/src/tooltip/tooltip.directive.ts b/libs/components/src/tooltip/tooltip.directive.ts index 153fecfe7bf..b2c1621d710 100644 --- a/libs/components/src/tooltip/tooltip.directive.ts +++ b/libs/components/src/tooltip/tooltip.directive.ts @@ -39,7 +39,7 @@ export class TooltipDirective implements OnInit { */ readonly tooltipPosition = input("above-center"); - private isVisible = signal(false); + private readonly isVisible = signal(false); private overlayRef: OverlayRef | undefined; private elementRef = inject(ElementRef); private overlay = inject(Overlay); diff --git a/libs/components/src/tooltip/tooltip.spec.ts b/libs/components/src/tooltip/tooltip.spec.ts index 57e05e4f65f..b6a49acbc77 100644 --- a/libs/components/src/tooltip/tooltip.spec.ts +++ b/libs/components/src/tooltip/tooltip.spec.ts @@ -12,6 +12,8 @@ import { Observable, Subject } from "rxjs"; import { TooltipDirective } from "./tooltip.directive"; +// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush +// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ standalone: true, imports: [TooltipDirective], From 63cdae92be538caca3b13c20cd744ba48667b6d4 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Tue, 21 Oct 2025 19:06:55 +0200 Subject: [PATCH 09/16] [PM-2348] Close popup window before process reload (#16795) * Close popup window before process reload * unit test coverage --- .../browser/src/background/main.background.ts | 7 +++ .../browser/browser-popup-utils.spec.ts | 62 +++++++++++++++++++ .../platform/browser/browser-popup-utils.ts | 22 +++++++ apps/browser/src/popup/app.component.ts | 9 +++ 4 files changed, 100 insertions(+) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index c4c412732c9..bcceac6fb84 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -300,6 +300,7 @@ import { BrowserActionsService } from "../platform/actions/browser-actions.servi import { DefaultBadgeBrowserApi } from "../platform/badge/badge-browser-api"; import { BadgeService } from "../platform/badge/badge.service"; import { BrowserApi } from "../platform/browser/browser-api"; +import BrowserPopupUtils from "../platform/browser/browser-popup-utils"; import { flagEnabled } from "../platform/flags"; import { IpcBackgroundService } from "../platform/ipc/ipc-background.service"; import { IpcContentScriptManagerService } from "../platform/ipc/ipc-content-script-manager.service"; @@ -1237,6 +1238,12 @@ export default class MainBackground { const systemUtilsServiceReloadCallback = async () => { await this.taskSchedulerService.clearAllScheduledTasks(); + + // Close browser action popup before reloading to prevent zombie popup with invalidated context. + // The 'reloadProcess' message is sent by ProcessReloadService before this callback runs, + // and popups will close themselves upon receiving it. Poll to verify popup is actually closed. + await BrowserPopupUtils.waitForAllPopupsClose(); + BrowserApi.reloadExtension(); }; diff --git a/apps/browser/src/platform/browser/browser-popup-utils.spec.ts b/apps/browser/src/platform/browser/browser-popup-utils.spec.ts index 9f9a6e313c8..e4165348c6e 100644 --- a/apps/browser/src/platform/browser/browser-popup-utils.spec.ts +++ b/apps/browser/src/platform/browser/browser-popup-utils.spec.ts @@ -337,6 +337,68 @@ describe("BrowserPopupUtils", () => { }); }); + describe("waitForAllPopupsClose", () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("should resolve immediately if no popups are open", async () => { + jest.spyOn(BrowserApi, "isPopupOpen").mockResolvedValue(false); + + const promise = BrowserPopupUtils.waitForAllPopupsClose(); + jest.advanceTimersByTime(100); + + await expect(promise).resolves.toBeUndefined(); + expect(BrowserApi.isPopupOpen).toHaveBeenCalledTimes(1); + }); + + it("should resolve after timeout if popup never closes when using custom timeout", async () => { + jest.spyOn(BrowserApi, "isPopupOpen").mockResolvedValue(true); + + const promise = BrowserPopupUtils.waitForAllPopupsClose(500); + + // Advance past the timeout + jest.advanceTimersByTime(600); + + await expect(promise).resolves.toBeUndefined(); + }); + + it("should resolve after timeout if popup never closes when using default timeout", async () => { + jest.spyOn(BrowserApi, "isPopupOpen").mockResolvedValue(true); + + const promise = BrowserPopupUtils.waitForAllPopupsClose(); + + // Advance past the default timeout + jest.advanceTimersByTime(1100); + + await expect(promise).resolves.toBeUndefined(); + }); + + it("should stop polling after popup closes before timeout", async () => { + let callCount = 0; + jest.spyOn(BrowserApi, "isPopupOpen").mockImplementation(async () => { + callCount++; + return callCount <= 2; + }); + + const promise = BrowserPopupUtils.waitForAllPopupsClose(1000); + + // Advance to when popup closes (300ms) + jest.advanceTimersByTime(300); + + await expect(promise).resolves.toBeUndefined(); + + // Advance further to ensure no more calls are made + jest.advanceTimersByTime(1000); + + expect(BrowserApi.isPopupOpen).toHaveBeenCalledTimes(3); + }); + }); + describe("isSingleActionPopoutOpen", () => { const windowOptions = { id: 1, diff --git a/apps/browser/src/platform/browser/browser-popup-utils.ts b/apps/browser/src/platform/browser/browser-popup-utils.ts index aebb3e92113..cd55f6361a0 100644 --- a/apps/browser/src/platform/browser/browser-popup-utils.ts +++ b/apps/browser/src/platform/browser/browser-popup-utils.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { filter, firstValueFrom, interval, of, switchMap, takeWhile, timeout } from "rxjs"; import { ScrollOptions } from "./abstractions/browser-popup-utils.abstractions"; import { BrowserApi } from "./browser-api"; @@ -212,6 +213,27 @@ export default class BrowserPopupUtils { } } + /** + * Waits for all browser action popups to close, polling up to the specified timeout. + * Used before extension reload to prevent zombie popups with invalidated contexts. + * + * @param timeoutMs - Maximum time to wait in milliseconds. Defaults to 1 second. + * @returns Promise that resolves when all popups are closed or timeout is reached. + */ + static async waitForAllPopupsClose(timeoutMs = 1000): Promise { + await firstValueFrom( + interval(100).pipe( + switchMap(() => BrowserApi.isPopupOpen()), + takeWhile((isOpen) => isOpen, true), + filter((isOpen) => !isOpen), + timeout({ + first: timeoutMs, + with: () => of(true), + }), + ), + ); + } + /** * Identifies if a single action window is open based on the passed popoutKey. * Will focus the existing window, and close any other windows that might exist diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 998531488d3..b85da665fa0 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -36,6 +36,7 @@ import { LogoutReason, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; +import { BrowserApi } from "@bitwarden/browser/platform/browser/browser-api"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; @@ -58,6 +59,7 @@ import { } from "@bitwarden/components"; import { BiometricsService, BiometricStateService, KeyService } from "@bitwarden/key-management"; +import BrowserPopupUtils from "../platform/browser/browser-popup-utils"; import { PopupCompactModeService } from "../platform/popup/layout/popup-compact-mode.service"; import { PopupSizeService } from "../platform/popup/layout/popup-size.service"; import { initPopupClosedListener } from "../platform/services/popup-view-cache-background.service"; @@ -286,6 +288,13 @@ export class AppComponent implements OnInit, OnDestroy { await this.biometricStateService.updateLastProcessReload(); window.location.reload(); }, 2000); + } else { + // Close browser action popup before extension reload to prevent zombie popup with invalidated context. + // This issue occurs in Chromium-based browsers (Chrome, Vivaldi, etc.) where chrome.runtime.reload() + // invalidates extension contexts before popup can close naturally + if (BrowserPopupUtils.inPopup(window)) { + BrowserApi.closePopup(window); + } } } else if (msg.command === "reloadPopup") { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. From 8beb1c6ab05f6c298d77e8345eded95be3ad27b8 Mon Sep 17 00:00:00 2001 From: Matt Andreko Date: Tue, 21 Oct 2025 13:13:45 -0400 Subject: [PATCH 10/16] Clean up workflow files from Zizmor output (#16690) --- .../workflows/alert-ddg-files-modified.yml | 5 +- .github/workflows/auto-branch-updater.yml | 11 +- .github/workflows/build-browser.yml | 62 +++-- .github/workflows/build-cli.yml | 76 ++++-- .github/workflows/build-desktop.yml | 243 ++++++++++-------- .github/workflows/build-web.yml | 59 +++-- .github/workflows/chromatic.yml | 3 +- .github/workflows/crowdin-pull.yml | 1 + .github/workflows/deploy-web.yml | 130 +++++----- .github/workflows/lint-crowdin-config.yml | 1 + .github/workflows/lint.yml | 6 +- .github/workflows/locales-lint.yml | 6 +- .github/workflows/nx.yml | 3 +- .github/workflows/publish-cli.yml | 28 +- .github/workflows/publish-desktop.yml | 72 ++++-- .github/workflows/publish-web.yml | 32 ++- .github/workflows/release-browser.yml | 22 +- .github/workflows/release-cli.yml | 2 + .github/workflows/release-desktop.yml | 14 +- .github/workflows/release-web.yml | 8 +- .github/workflows/repository-management.yml | 100 +++---- .../retrieve-current-desktop-rollout.yml | 4 +- .github/workflows/staged-rollout-desktop.yml | 12 +- .../workflows/test-browser-interactions.yml | 1 + .github/workflows/test.yml | 10 +- .github/workflows/version-auto-bump.yml | 7 +- 26 files changed, 536 insertions(+), 382 deletions(-) diff --git a/.github/workflows/alert-ddg-files-modified.yml b/.github/workflows/alert-ddg-files-modified.yml index d799cc2e248..84cd67ecd5b 100644 --- a/.github/workflows/alert-ddg-files-modified.yml +++ b/.github/workflows/alert-ddg-files-modified.yml @@ -17,6 +17,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - name: Get changed files id: changed-files @@ -68,9 +69,11 @@ jobs: - name: Comment on PR if monitored files changed if: steps.changed-files.outputs.monitored == 'true' uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + _MONITORED_FILES: ${{ steps.changed-files.outputs.monitored_files }} with: script: | - const changedFiles = `${{ steps.changed-files.outputs.monitored_files }}`.split(' ').filter(file => file.trim() !== ''); + const changedFiles = `$_MONITORED_FILES`.split(' ').filter(file => file.trim() !== ''); const message = ` ⚠️🦆 **DuckDuckGo Integration files have been modified in this PR:** diff --git a/.github/workflows/auto-branch-updater.yml b/.github/workflows/auto-branch-updater.yml index 3f67388fd0c..ceebfb7e466 100644 --- a/.github/workflows/auto-branch-updater.yml +++ b/.github/workflows/auto-branch-updater.yml @@ -27,17 +27,20 @@ jobs: steps: - name: Setup id: setup - run: echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT + run: echo "branch=${GITHUB_REF#refs/heads/}" >> "$GITHUB_OUTPUT" - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: 'eu-web-${{ steps.setup.outputs.branch }}' fetch-depth: 0 + persist-credentials: true - name: Merge ${{ steps.setup.outputs.branch }} + env: + _BRANCH: ${{ steps.setup.outputs.branch }} run: | - git config --local user.email "${{ env._BOT_EMAIL }}" - git config --local user.name "${{ env._BOT_NAME }}" - git merge origin/${{ steps.setup.outputs.branch }} + git config --local user.email "$_BOT_EMAIL" + git config --local user.name "$_BOT_NAME" + git merge "origin/$_BRANCH" git push diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index bece680b9d0..e3a49e414f9 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -58,15 +58,16 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Get Package Version id: gen_vars run: | - repo_url=https://github.com/$GITHUB_REPOSITORY.git + repo_url="https://github.com/$GITHUB_REPOSITORY.git" adj_build_num=${GITHUB_SHA:0:7} - echo "repo_url=$repo_url" >> $GITHUB_OUTPUT - echo "adj_build_number=$adj_build_num" >> $GITHUB_OUTPUT + echo "repo_url=$repo_url" >> "$GITHUB_OUTPUT" + echo "adj_build_number=$adj_build_num" >> "$GITHUB_OUTPUT" - name: Get Node Version id: retrieve-node-version @@ -74,13 +75,13 @@ jobs: run: | NODE_NVMRC=$(cat .nvmrc) NODE_VERSION=${NODE_NVMRC/v/''} - echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT + echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT" - name: Check secrets id: check-secrets run: | has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }} - echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT + echo "has_secrets=$has_secrets" >> "$GITHUB_OUTPUT" locales-test: @@ -96,6 +97,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Testing locales - extName length run: | @@ -105,12 +107,14 @@ jobs: echo "============" echo "extName string must be 40 characters or less" echo - for locale in $(ls src/_locales/); do - string_length=$(jq '.extName.message | length' src/_locales/$locale/messages.json) - if [[ $string_length -gt 40 ]]; then - echo "$locale: $string_length" - found_error=true - fi + + for locale_path in src/_locales/*/messages.json; do + locale=$(basename "$(dirname "$locale_path")") + string_length=$(jq '.extName.message | length' "$locale_path") + if [ "$string_length" -gt 40 ]; then + echo "$locale: $string_length" + found_error=true + fi done if $found_error; then @@ -145,6 +149,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Set up Node uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 @@ -246,6 +251,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Set up Node uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 @@ -301,13 +307,13 @@ jobs: TARGET_DIR='./browser-source/apps/browser' while IFS=' ' read -r RESULT; do FILES+=("$RESULT") - done < <(find $TARGET_DIR -size +5M) + done < <(find "$TARGET_DIR" -size +5M) # Validate results and provide messaging if [[ ${#FILES[@]} -ne 0 ]]; then echo "File(s) exceeds size limit: 5MB" - for FILE in ${FILES[@]}; do - echo "- $(du --si $FILE)" + for FILE in "${FILES[@]}"; do + echo "- $(du --si "$FILE")" done echo "ERROR Firefox rejects extension uploads that contain files larger than 5MB" # Invoke failure @@ -357,6 +363,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Set up Node uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 @@ -389,34 +396,34 @@ jobs: ACCOUNT_NAME: bitwardenci CONTAINER_NAME: profiles run: | - mkdir -p $HOME/secrets + mkdir -p "$HOME/secrets" - az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ + az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \ --name bitwarden_desktop_appstore.provisionprofile \ - --file $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ + --file "$HOME/secrets/bitwarden_desktop_appstore.provisionprofile" \ --output none - name: Get certificates run: | - mkdir -p $HOME/certificates + mkdir -p "$HOME/certificates" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/bitwarden-desktop-key | - jq -r .value | base64 -d > $HOME/certificates/bitwarden-desktop-key.p12 + jq -r .value | base64 -d > "$HOME/certificates/bitwarden-desktop-key.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/appstore-app-cert | - jq -r .value | base64 -d > $HOME/certificates/appstore-app-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/appstore-app-cert.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/appstore-installer-cert | - jq -r .value | base64 -d > $HOME/certificates/appstore-installer-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/appstore-installer-cert.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-app-cert | - jq -r .value | base64 -d > $HOME/certificates/devid-app-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/devid-app-cert.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-installer-cert | - jq -r .value | base64 -d > $HOME/certificates/devid-installer-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/devid-installer-cert.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert | - jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/macdev-cert.p12" - name: Log out from Azure uses: bitwarden/gh-actions/azure-logout@main @@ -425,9 +432,9 @@ jobs: env: KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }} run: | - security create-keychain -p $KEYCHAIN_PASSWORD build.keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security default-keychain -s build.keychain - security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security set-keychain-settings -lut 1200 build.keychain security import "$HOME/certificates/bitwarden-desktop-key.p12" -k build.keychain -P "" \ @@ -448,7 +455,7 @@ jobs: security import "$HOME/certificates/macdev-cert.p12" -k build.keychain -P "" \ -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain - name: NPM setup run: npm ci @@ -507,6 +514,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Log in to Azure uses: bitwarden/gh-actions/azure-login@main diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index d1df280f764..839181c6107 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -62,26 +62,27 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Get Package Version id: retrieve-package-version run: | PKG_VERSION=$(jq -r .version package.json) - echo "package_version=$PKG_VERSION" >> $GITHUB_OUTPUT + echo "package_version=$PKG_VERSION" >> "$GITHUB_OUTPUT" - name: Get Node Version id: retrieve-node-version working-directory: ./ run: | NODE_NVMRC=$(cat .nvmrc) - NODE_VERSION=${NODE_NVMRC/v/''} - echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT + NODE_VERSION="${NODE_NVMRC/v/''}" + echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT" - name: Check secrets id: check-secrets run: | has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }} - echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT + echo "has_secrets=$has_secrets" >> "$GITHUB_OUTPUT" cli: @@ -116,12 +117,17 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Setup Unix Vars run: | - echo "LOWER_RUNNER_OS=$(echo $RUNNER_OS | awk '{print tolower($0)}')" >> $GITHUB_ENV - echo "SHORT_RUNNER_OS=$(echo $RUNNER_OS | awk '{print substr($0, 1, 3)}' | \ - awk '{print tolower($0)}')" >> $GITHUB_ENV + LOWER_RUNNER_OS="$(printf '%s' "$RUNNER_OS" | awk '{print tolower($0)}')" + SHORT_RUNNER_OS="$(printf '%s' "$RUNNER_OS" | awk '{print substr($0, 1, 3)}' | awk '{print tolower($0)}')" + + { + echo "LOWER_RUNNER_OS=$LOWER_RUNNER_OS" + echo "SHORT_RUNNER_OS=$SHORT_RUNNER_OS" + } >> "$GITHUB_ENV" - name: Set up Node uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 @@ -155,7 +161,9 @@ jobs: npm link ../sdk-internal - name: Build & Package Unix - run: npm run dist:${{ matrix.license_type.build_prefix }}:${{ env.SHORT_RUNNER_OS }}${{ matrix.os.target_suffix }} --quiet + env: + _SHORT_RUNNER_OS: ${{ env.SHORT_RUNNER_OS }} + run: npm run "dist:${{ matrix.license_type.build_prefix }}:$_SHORT_RUNNER_OS${{ matrix.os.target_suffix }}" --quiet - name: Login to Azure if: ${{ matrix.os.base == 'mac' && needs.setup.outputs.has_secrets == 'true' }} @@ -168,10 +176,10 @@ jobs: - name: Get certificates if: ${{ matrix.os.base == 'mac' && needs.setup.outputs.has_secrets == 'true' }} run: | - mkdir -p $HOME/certificates + mkdir -p "$HOME/certificates" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-app-cert | - jq -r .value | base64 -d > $HOME/certificates/devid-app-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/devid-app-cert.p12" - name: Get Azure Key Vault secrets id: get-kv-secrets @@ -189,33 +197,39 @@ jobs: env: KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }} run: | - security create-keychain -p $KEYCHAIN_PASSWORD build.keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security default-keychain -s build.keychain - security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security set-keychain-settings -lut 1200 build.keychain security import "$HOME/certificates/devid-app-cert.p12" -k build.keychain -P "" \ -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain - name: Sign binary if: ${{ matrix.os.base == 'mac' && needs.setup.outputs.has_secrets == 'true' }} env: MACOS_CERTIFICATE_NAME: "Developer ID Application: 8bit Solutions LLC" - run: codesign --sign "$MACOS_CERTIFICATE_NAME" --verbose=3 --force --options=runtime --entitlements ./entitlements.plist --timestamp ./dist/${{ matrix.license_type.build_prefix }}/${{ env.LOWER_RUNNER_OS }}${{ matrix.os.target_suffix }}/bw + _LOWER_RUNNER_OS: ${{ env.LOWER_RUNNER_OS }} + run: codesign --sign "$MACOS_CERTIFICATE_NAME" --verbose=3 --force --options=runtime --entitlements ./entitlements.plist --timestamp "./dist/${{ matrix.license_type.build_prefix }}/$_LOWER_RUNNER_OS${{ matrix.os.target_suffix }}/bw" - name: Zip Unix + env: + _LOWER_RUNNER_OS: ${{ env.LOWER_RUNNER_OS }} + _PACKAGE_VERSION: ${{ env._PACKAGE_VERSION }} run: | - cd ./dist/${{ matrix.license_type.build_prefix }}/${{ env.LOWER_RUNNER_OS }}${{ matrix.os.target_suffix }} - zip ../../bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}${{ matrix.os.target_suffix }}-${{ env._PACKAGE_VERSION }}.zip ./bw + cd "./dist/${{ matrix.license_type.build_prefix }}/$_LOWER_RUNNER_OS${{ matrix.os.target_suffix }}" + zip "../../bw${{ matrix.license_type.artifact_prefix }}-$_LOWER_RUNNER_OS${{ matrix.os.target_suffix }}-$_PACKAGE_VERSION.zip" ./bw - name: Set up private auth key if: ${{ matrix.os.base == 'mac' && needs.setup.outputs.has_secrets == 'true' }} + env: + _APP_STORE_CONNECT_AUTH_KEY: ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-AUTH-KEY }} run: | mkdir ~/private_keys cat << EOF > ~/private_keys/AuthKey_6TV9MKN3GP.p8 - ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-AUTH-KEY }} + $_APP_STORE_CONNECT_AUTH_KEY EOF - name: Notarize app @@ -224,22 +238,26 @@ jobs: APP_STORE_CONNECT_TEAM_ISSUER: ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-TEAM-ISSUER }} APP_STORE_CONNECT_AUTH_KEY: 6TV9MKN3GP APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_6TV9MKN3GP.p8 + _LOWER_RUNNER_OS: ${{ env.LOWER_RUNNER_OS }} run: | echo "Create keychain profile" xcrun notarytool store-credentials "notarytool-profile" --key-id "$APP_STORE_CONNECT_AUTH_KEY" --key "$APP_STORE_CONNECT_AUTH_KEY_PATH" --issuer "$APP_STORE_CONNECT_TEAM_ISSUER" - codesign --sign "Developer ID Application: 8bit Solutions LLC" --verbose=3 --force --options=runtime --timestamp ./dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}${{ matrix.os.target_suffix }}-${{ env._PACKAGE_VERSION }}.zip + codesign --sign "Developer ID Application: 8bit Solutions LLC" --verbose=3 --force --options=runtime --timestamp "./dist/bw${{ matrix.license_type.artifact_prefix }}-$_LOWER_RUNNER_OS${{ matrix.os.target_suffix }}-$_PACKAGE_VERSION.zip" echo "Notarize app" - xcrun notarytool submit ./dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}${{ matrix.os.target_suffix }}-${{ env._PACKAGE_VERSION }}.zip --keychain-profile "notarytool-profile" --wait + xcrun notarytool submit "./dist/bw${{ matrix.license_type.artifact_prefix }}-$_LOWER_RUNNER_OS${{ matrix.os.target_suffix }}-$_PACKAGE_VERSION.zip" --keychain-profile "notarytool-profile" --wait - name: Version Test + env: + _PACKAGE_VERSION: ${{ env._PACKAGE_VERSION }} + _LOWER_RUNNER_OS: ${{ env.LOWER_RUNNER_OS }} run: | - unzip "./dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}${{ matrix.os.target_suffix }}-${{ env._PACKAGE_VERSION }}.zip" -d "./test" + unzip "./dist/bw${{ matrix.license_type.artifact_prefix }}-$_LOWER_RUNNER_OS${{ matrix.os.target_suffix }}-$_PACKAGE_VERSION.zip" -d "./test" testVersion=$(./test/bw -v) echo "version: $_PACKAGE_VERSION" echo "testVersion: $testVersion" - if [[ $testVersion != $_PACKAGE_VERSION ]]; then + if [[ $testVersion != "$_PACKAGE_VERSION" ]]; then echo "Version test failed." exit 1 fi @@ -291,6 +309,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Install AST run: dotnet tool install --global AzureSignTool --version 4.0.1 @@ -429,11 +448,13 @@ jobs: - name: Package Chocolatey shell: pwsh if: ${{ matrix.license_type.build_prefix == 'bit' }} + env: + _PACKAGE_VERSION: ${{ env._PACKAGE_VERSION }} run: | Copy-Item -Path stores/chocolatey -Destination dist/chocolatey -Recurse Copy-Item dist/${{ matrix.license_type.build_prefix }}/windows/bw.exe -Destination dist/chocolatey/tools Copy-Item ${{ github.workspace }}/LICENSE.txt -Destination dist/chocolatey/tools - choco pack dist/chocolatey/bitwarden-cli.nuspec --version ${{ env._PACKAGE_VERSION }} --out dist/chocolatey + choco pack dist/chocolatey/bitwarden-cli.nuspec --version "$env:_PACKAGE_VERSION" --out dist/chocolatey - name: Zip Windows shell: cmd @@ -466,7 +487,9 @@ jobs: if-no-files-found: error - name: Zip NPM Build Artifact - run: Get-ChildItem -Path .\build | Compress-Archive -DestinationPath .\bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip + env: + _PACKAGE_VERSION: ${{ env._PACKAGE_VERSION }} + run: Get-ChildItem -Path .\build | Compress-Archive -DestinationPath ".\bitwarden-cli-${env:_PACKAGE_VERSION}-npm-build.zip" - name: Upload NPM Build Directory asset if: matrix.license_type.build_prefix == 'bit' @@ -490,8 +513,11 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Print environment + env: + _PACKAGE_VERSION: ${{ env._PACKAGE_VERSION }} run: | whoami echo "GitHub ref: $GITHUB_REF" @@ -505,9 +531,11 @@ jobs: path: apps/cli/dist/snap - name: Setup Snap Package + env: + _PACKAGE_VERSION: ${{ env._PACKAGE_VERSION }} run: | cp -r stores/snap/* -t dist/snap - sed -i s/__version__/${{ env._PACKAGE_VERSION }}/g dist/snap/snapcraft.yaml + sed -i "s/__version__/$_PACKAGE_VERSION/g" "dist/snap/snapcraft.yaml" cd dist/snap ls -alth diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 8ba5f981453..51a0938552c 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -58,6 +58,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Verify run: | @@ -90,35 +91,38 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: true - name: Get Package Version id: retrieve-version run: | PKG_VERSION=$(jq -r .version src/package.json) echo "Setting version number to $PKG_VERSION" - echo "package_version=$PKG_VERSION" >> $GITHUB_OUTPUT + echo "package_version=$PKG_VERSION" >> "$GITHUB_OUTPUT" - name: Increment Version id: increment-version run: | - BUILD_NUMBER=$(expr 3000 + $GITHUB_RUN_NUMBER) + BUILD_NUMBER=$((3000 + GITHUB_RUN_NUMBER)) echo "Setting build number to $BUILD_NUMBER" - echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT + echo "build_number=$BUILD_NUMBER" >> "$GITHUB_OUTPUT" - name: Get Version Channel id: release-channel + env: + _PACKAGE_VERSION: ${{ steps.retrieve-version.outputs.package_version }} run: | - case "${{ steps.retrieve-version.outputs.package_version }}" in + case "$_PACKAGE_VERSION" in *"alpha"*) - echo "channel=alpha" >> $GITHUB_OUTPUT + echo "channel=alpha" >> "$GITHUB_OUTPUT" echo "[!] We do not yet support 'alpha'" exit 1 ;; *"beta"*) - echo "channel=beta" >> $GITHUB_OUTPUT + echo "channel=beta" >> "$GITHUB_OUTPUT" ;; *) - echo "channel=latest" >> $GITHUB_OUTPUT + echo "channel=latest" >> "$GITHUB_OUTPUT" ;; esac @@ -126,15 +130,15 @@ jobs: id: branch-check run: | if [[ $(git ls-remote --heads origin rc) ]]; then - echo "rc_branch_exists=1" >> $GITHUB_OUTPUT + echo "rc_branch_exists=1" >> "$GITHUB_OUTPUT" else - echo "rc_branch_exists=0" >> $GITHUB_OUTPUT + echo "rc_branch_exists=0" >> "$GITHUB_OUTPUT" fi if [[ $(git ls-remote --heads origin hotfix-rc-desktop) ]]; then - echo "hotfix_branch_exists=1" >> $GITHUB_OUTPUT + echo "hotfix_branch_exists=1" >> "$GITHUB_OUTPUT" else - echo "hotfix_branch_exists=0" >> $GITHUB_OUTPUT + echo "hotfix_branch_exists=0" >> "$GITHUB_OUTPUT" fi - name: Get Node Version @@ -143,13 +147,13 @@ jobs: run: | NODE_NVMRC=$(cat .nvmrc) NODE_VERSION=${NODE_NVMRC/v/''} - echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT + echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT" - name: Check secrets id: check-secrets run: | has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }} - echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT + echo "has_secrets=$has_secrets" >> "$GITHUB_OUTPUT" linux: name: Linux Build @@ -172,6 +176,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Set up Node uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 @@ -321,6 +326,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Set up Node uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 @@ -427,6 +433,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Set up Node uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 @@ -533,21 +540,21 @@ jobs: - name: Rename appx files for store if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | - Copy-Item "./dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx" ` - -Destination "./dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx" - Copy-Item "./dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx" ` - -Destination "./dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx" - Copy-Item "./dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx" ` - -Destination "./dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx" + Copy-Item "./dist/Bitwarden-$env:_PACKAGE_VERSION-ia32.appx" ` + -Destination "./dist/Bitwarden-$env:_PACKAGE_VERSION-ia32-store.appx" + Copy-Item "./dist/Bitwarden-$env:_PACKAGE_VERSION-x64.appx" ` + -Destination "./dist/Bitwarden-$env:_PACKAGE_VERSION-x64-store.appx" + Copy-Item "./dist/Bitwarden-$env:_PACKAGE_VERSION-arm64.appx" ` + -Destination "./dist/Bitwarden-$env:_PACKAGE_VERSION-arm64-store.appx" - name: Package for Chocolatey if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | Copy-Item -Path ./stores/chocolatey -Destination ./dist/chocolatey -Recurse - Copy-Item -Path ./dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe ` + Copy-Item -Path ./dist/nsis-web/Bitwarden-Installer-$env:_PACKAGE_VERSION.exe ` -Destination ./dist/chocolatey - $checksum = checksum -t sha256 ./dist/chocolatey/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe + $checksum = checksum -t sha256 ./dist/chocolatey/Bitwarden-Installer-$env:_PACKAGE_VERSION.exe $chocoInstall = "./dist/chocolatey/tools/chocolateyinstall.ps1" (Get-Content $chocoInstall).replace('__version__', "$env:_PACKAGE_VERSION").replace('__checksum__', $checksum) | Set-Content $chocoInstall choco pack ./dist/chocolatey/bitwarden.nuspec --version "$env:_PACKAGE_VERSION" --out ./dist/chocolatey @@ -555,12 +562,12 @@ jobs: - name: Fix NSIS artifact names for auto-updater if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | - Rename-Item -Path .\dist\nsis-web\Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z ` - -NewName bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z - Rename-Item -Path .\dist\nsis-web\Bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z ` - -NewName bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z - Rename-Item -Path .\dist\nsis-web\Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z ` - -NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z + Rename-Item -Path .\dist\nsis-web\Bitwarden-$env:_PACKAGE_VERSION-ia32.nsis.7z ` + -NewName bitwarden-$env:_PACKAGE_VERSION-ia32.nsis.7z + Rename-Item -Path .\dist\nsis-web\Bitwarden-$env:_PACKAGE_VERSION-x64.nsis.7z ` + -NewName bitwarden-$env:_PACKAGE_VERSION-x64.nsis.7z + Rename-Item -Path .\dist\nsis-web\Bitwarden-$env:_PACKAGE_VERSION-arm64.nsis.7z ` + -NewName bitwarden-$env:_PACKAGE_VERSION-arm64.nsis.7z - name: Upload portable exe artifact uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 @@ -573,7 +580,7 @@ jobs: if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: - name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe + name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}..exe path: apps/desktop/dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe if-no-files-found: error @@ -919,6 +926,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Set up Node uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 @@ -974,40 +982,40 @@ jobs: ACCOUNT_NAME: bitwardenci CONTAINER_NAME: profiles run: | - mkdir -p $HOME/secrets + mkdir -p "$HOME/secrets" - az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ + az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \ --name bitwarden_desktop_appstore.provisionprofile \ - --file $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ + --file "$HOME/secrets/bitwarden_desktop_appstore.provisionprofile" \ --output none - az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ + az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \ --name bitwarden_desktop_autofill_app_store_2024.provisionprofile \ - --file $HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile \ + --file "$HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile" \ --output none - name: Get certificates if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | - mkdir -p $HOME/certificates + mkdir -p "$HOME/certificates" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/bitwarden-desktop-key | - jq -r .value | base64 -d > $HOME/certificates/bitwarden-desktop-key.p12 + jq -r .value | base64 -d > "$HOME/certificates/bitwarden-desktop-key.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/appstore-app-cert | - jq -r .value | base64 -d > $HOME/certificates/appstore-app-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/appstore-app-cert.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/appstore-installer-cert | - jq -r .value | base64 -d > $HOME/certificates/appstore-installer-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/appstore-installer-cert.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-app-cert | - jq -r .value | base64 -d > $HOME/certificates/devid-app-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/devid-app-cert.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-installer-cert | - jq -r .value | base64 -d > $HOME/certificates/devid-installer-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/devid-installer-cert.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert | - jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/macdev-cert.p12" - name: Log out from Azure if: ${{ needs.setup.outputs.has_secrets == 'true' }} @@ -1018,9 +1026,9 @@ jobs: env: KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }} run: | - security create-keychain -p $KEYCHAIN_PASSWORD build.keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security default-keychain -s build.keychain - security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security set-keychain-settings -lut 1200 build.keychain security import "$HOME/certificates/bitwarden-desktop-key.p12" -k build.keychain -P "" \ @@ -1041,22 +1049,22 @@ jobs: security import "$HOME/certificates/macdev-cert.p12" -k build.keychain -P "" \ -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain - name: Set up provisioning profiles if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | - cp $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ - $GITHUB_WORKSPACE/apps/desktop/bitwarden_desktop_appstore.provisionprofile + cp "$HOME/secrets/bitwarden_desktop_appstore.provisionprofile" \ + "$GITHUB_WORKSPACE/apps/desktop/bitwarden_desktop_appstore.provisionprofile" - mkdir -p $HOME/Library/MobileDevice/Provisioning\ Profiles - export APP_UUID=`grep UUID -A1 -a $HOME/secrets/bitwarden_desktop_appstore.provisionprofile | grep -io "[-A-Z0-9]\{36\}"` - export AUTOFILL_UUID=`grep UUID -A1 -a $HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile | grep -io "[-A-Z0-9]\{36\}"` + mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles" + APP_UUID=$(grep UUID -A1 -a "$HOME/secrets/bitwarden_desktop_appstore.provisionprofile" | grep -io "[-A-Z0-9]\{36\}") + AUTOFILL_UUID=$(grep UUID -A1 -a "$HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile" | grep -io "[-A-Z0-9]\{36\}") - cp $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ - $HOME/Library/MobileDevice/Provisioning\ Profiles/$APP_UUID.provisionprofile - cp $HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile \ - $HOME/Library/MobileDevice/Provisioning\ Profiles/$AUTOFILL_UUID.provisionprofile + cp "$HOME/secrets/bitwarden_desktop_appstore.provisionprofile" \ + "$HOME/Library/MobileDevice/Provisioning Profiles/$APP_UUID.provisionprofile" + cp "$HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile" \ + "$HOME/Library/MobileDevice/Provisioning Profiles/$AUTOFILL_UUID.provisionprofile" - name: Increment version shell: pwsh @@ -1145,6 +1153,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Set up Node uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 @@ -1197,39 +1206,39 @@ jobs: ACCOUNT_NAME: bitwardenci CONTAINER_NAME: profiles run: | - mkdir -p $HOME/secrets + mkdir -p "$HOME/secrets" - az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ + az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \ --name bitwarden_desktop_developer_id.provisionprofile \ - --file $HOME/secrets/bitwarden_desktop_developer_id.provisionprofile \ + --file "$HOME/secrets/bitwarden_desktop_developer_id.provisionprofile" \ --output none - az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ + az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \ --name bitwarden_desktop_autofill_developer_id.provisionprofile \ - --file $HOME/secrets/bitwarden_desktop_autofill_developer_id.provisionprofile \ + --file "$HOME/secrets/bitwarden_desktop_autofill_developer_id.provisionprofile" \ --output none - name: Get certificates run: | - mkdir -p $HOME/certificates + mkdir -p "$HOME/certificates" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/bitwarden-desktop-key | - jq -r .value | base64 -d > $HOME/certificates/bitwarden-desktop-key.p12 + jq -r .value | base64 -d > "$HOME/certificates/bitwarden-desktop-key.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/appstore-app-cert | - jq -r .value | base64 -d > $HOME/certificates/appstore-app-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/appstore-app-cert.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/appstore-installer-cert | - jq -r .value | base64 -d > $HOME/certificates/appstore-installer-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/appstore-installer-cert.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-app-cert | - jq -r .value | base64 -d > $HOME/certificates/devid-app-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/devid-app-cert.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-installer-cert | - jq -r .value | base64 -d > $HOME/certificates/devid-installer-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/devid-installer-cert.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert | - jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/macdev-cert.p12" - name: Log out from Azure uses: bitwarden/gh-actions/azure-logout@main @@ -1238,9 +1247,9 @@ jobs: env: KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }} run: | - security create-keychain -p $KEYCHAIN_PASSWORD build.keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security default-keychain -s build.keychain - security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security set-keychain-settings -lut 1200 build.keychain security import "$HOME/certificates/bitwarden-desktop-key.p12" -k build.keychain -P "" \ @@ -1252,21 +1261,21 @@ jobs: security import "$HOME/certificates/devid-installer-cert.p12" -k build.keychain -P "" \ -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain - name: Set up provisioning profiles run: | - cp $HOME/secrets/bitwarden_desktop_developer_id.provisionprofile \ - $GITHUB_WORKSPACE/apps/desktop/bitwarden_desktop_developer_id.provisionprofile + cp "$HOME/secrets/bitwarden_desktop_developer_id.provisionprofile" \ + "$GITHUB_WORKSPACE/apps/desktop/bitwarden_desktop_developer_id.provisionprofile" - mkdir -p $HOME/Library/MobileDevice/Provisioning\ Profiles - export APP_UUID=`grep UUID -A1 -a $HOME/secrets/bitwarden_desktop_developer_id.provisionprofile | grep -io "[-A-Z0-9]\{36\}"` - export AUTOFILL_UUID=`grep UUID -A1 -a $HOME/secrets/bitwarden_desktop_autofill_developer_id.provisionprofile | grep -io "[-A-Z0-9]\{36\}"` + mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles" + APP_UUID=$(grep UUID -A1 -a "$HOME/secrets/bitwarden_desktop_developer_id.provisionprofile" | grep -io "[-A-Z0-9]\{36\}") + AUTOFILL_UUID=$(grep UUID -A1 -a "$HOME/secrets/bitwarden_desktop_autofill_developer_id.provisionprofile" | grep -io "[-A-Z0-9]\{36\}") - cp $HOME/secrets/bitwarden_desktop_developer_id.provisionprofile \ - $HOME/Library/MobileDevice/Provisioning\ Profiles/$APP_UUID.provisionprofile - cp $HOME/secrets/bitwarden_desktop_autofill_developer_id.provisionprofile \ - $HOME/Library/MobileDevice/Provisioning\ Profiles/$AUTOFILL_UUID.provisionprofile + cp "$HOME/secrets/bitwarden_desktop_developer_id.provisionprofile" \ + "$HOME/Library/MobileDevice/Provisioning Profiles/$APP_UUID.provisionprofile" + cp "$HOME/secrets/bitwarden_desktop_autofill_developer_id.provisionprofile" \ + "$HOME/Library/MobileDevice/Provisioning Profiles/$AUTOFILL_UUID.provisionprofile" - name: Increment version shell: pwsh @@ -1327,20 +1336,22 @@ jobs: - name: Unzip Safari artifact run: | - SAFARI_DIR=$(find $GITHUB_WORKSPACE/browser-build-artifacts -name 'dist-safari-*.zip') - echo $SAFARI_DIR - unzip $SAFARI_DIR/dist-safari.zip -d $GITHUB_WORKSPACE/browser-build-artifacts + SAFARI_DIR=$(find "$GITHUB_WORKSPACE/browser-build-artifacts" -name 'dist-safari-*.zip') + echo "$SAFARI_DIR" + unzip "$SAFARI_DIR/dist-safari.zip" -d "$GITHUB_WORKSPACE/browser-build-artifacts" - name: Load Safari extension for .dmg run: | mkdir PlugIns - cp -r $GITHUB_WORKSPACE/browser-build-artifacts/Safari/dmg/build/Release/safari.appex PlugIns/safari.appex + cp -r "$GITHUB_WORKSPACE/browser-build-artifacts/Safari/dmg/build/Release/safari.appex" PlugIns/safari.appex - name: Set up private auth key + env: + _APP_STORE_CONNECT_AUTH_KEY: ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-AUTH-KEY }} run: | mkdir ~/private_keys cat << EOF > ~/private_keys/AuthKey_6TV9MKN3GP.p8 - ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-AUTH-KEY }} + $_APP_STORE_CONNECT_AUTH_KEY EOF - name: Build application (dist) @@ -1403,6 +1414,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Set up Node uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 @@ -1462,39 +1474,39 @@ jobs: ACCOUNT_NAME: bitwardenci CONTAINER_NAME: profiles run: | - mkdir -p $HOME/secrets + mkdir -p "$HOME/secrets" - az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ + az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \ --name bitwarden_desktop_appstore.provisionprofile \ - --file $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ + --file "$HOME/secrets/bitwarden_desktop_appstore.provisionprofile" \ --output none - az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \ + az storage blob download --account-name "$ACCOUNT_NAME" --container-name "$CONTAINER_NAME" \ --name bitwarden_desktop_autofill_app_store_2024.provisionprofile \ - --file $HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile \ + --file "$HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile" \ --output none - name: Get certificates run: | - mkdir -p $HOME/certificates + mkdir -p "$HOME/certificates" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/bitwarden-desktop-key | - jq -r .value | base64 -d > $HOME/certificates/bitwarden-desktop-key.p12 + jq -r .value | base64 -d > "$HOME/certificates/bitwarden-desktop-key.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/appstore-app-cert | - jq -r .value | base64 -d > $HOME/certificates/appstore-app-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/appstore-app-cert.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/appstore-installer-cert | - jq -r .value | base64 -d > $HOME/certificates/appstore-installer-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/appstore-installer-cert.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-app-cert | - jq -r .value | base64 -d > $HOME/certificates/devid-app-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/devid-app-cert.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-installer-cert | - jq -r .value | base64 -d > $HOME/certificates/devid-installer-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/devid-installer-cert.p12" az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert | - jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12 + jq -r .value | base64 -d > "$HOME/certificates/macdev-cert.p12" - name: Log out from Azure uses: bitwarden/gh-actions/azure-logout@main @@ -1503,9 +1515,9 @@ jobs: env: KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }} run: | - security create-keychain -p $KEYCHAIN_PASSWORD build.keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security default-keychain -s build.keychain - security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security set-keychain-settings -lut 1200 build.keychain security import "$HOME/certificates/bitwarden-desktop-key.p12" -k build.keychain -P "" \ @@ -1517,21 +1529,21 @@ jobs: security import "$HOME/certificates/appstore-installer-cert.p12" -k build.keychain -P "" \ -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain - name: Set up provisioning profiles run: | - cp $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ - $GITHUB_WORKSPACE/apps/desktop/bitwarden_desktop_appstore.provisionprofile + cp "$HOME/secrets/bitwarden_desktop_appstore.provisionprofile" \ + "$GITHUB_WORKSPACE/apps/desktop/bitwarden_desktop_appstore.provisionprofile" - mkdir -p $HOME/Library/MobileDevice/Provisioning\ Profiles - export APP_UUID=`grep UUID -A1 -a $HOME/secrets/bitwarden_desktop_appstore.provisionprofile | grep -io "[-A-Z0-9]\{36\}"` - export AUTOFILL_UUID=`grep UUID -A1 -a $HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile | grep -io "[-A-Z0-9]\{36\}"` + mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles" + APP_UUID=$(grep UUID -A1 -a "$HOME/secrets/bitwarden_desktop_appstore.provisionprofile" | grep -io "[-A-Z0-9]\{36\}") + AUTOFILL_UUID=$(grep UUID -A1 -a "$HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile" | grep -io "[-A-Z0-9]\{36\}") - cp $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ - $HOME/Library/MobileDevice/Provisioning\ Profiles/$APP_UUID.provisionprofile - cp $HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile \ - $HOME/Library/MobileDevice/Provisioning\ Profiles/$AUTOFILL_UUID.provisionprofile + cp "$HOME/secrets/bitwarden_desktop_appstore.provisionprofile" \ + "$HOME/Library/MobileDevice/Provisioning Profiles/$APP_UUID.provisionprofile" + cp "$HOME/secrets/bitwarden_desktop_autofill_app_store_2024.provisionprofile" \ + "$HOME/Library/MobileDevice/Provisioning Profiles/$AUTOFILL_UUID.provisionprofile" - name: Increment version shell: pwsh @@ -1593,20 +1605,22 @@ jobs: - name: Unzip Safari artifact run: | - SAFARI_DIR=$(find $GITHUB_WORKSPACE/browser-build-artifacts -name 'dist-safari-*.zip') - echo $SAFARI_DIR - unzip $SAFARI_DIR/dist-safari.zip -d $GITHUB_WORKSPACE/browser-build-artifacts + SAFARI_DIR=$(find "$GITHUB_WORKSPACE/browser-build-artifacts" -name 'dist-safari-*.zip') + echo "$SAFARI_DIR" + unzip "$SAFARI_DIR/dist-safari.zip" -d "$GITHUB_WORKSPACE/browser-build-artifacts" - name: Load Safari extension for App Store run: | mkdir PlugIns - cp -r $GITHUB_WORKSPACE/browser-build-artifacts/Safari/mas/build/Release/safari.appex PlugIns/safari.appex + cp -r "$GITHUB_WORKSPACE/browser-build-artifacts/Safari/mas/build/Release/safari.appex" "PlugIns/safari.appex" - name: Set up private auth key + env: + _APP_STORE_CONNECT_AUTH_KEY: ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-AUTH-KEY }} run: | mkdir ~/private_keys cat << EOF > ~/private_keys/AuthKey_6TV9MKN3GP.p8 - ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-AUTH-KEY }} + $_APP_STORE_CONNECT_AUTH_KEY EOF - name: Build application for App Store @@ -1645,6 +1659,8 @@ jobs: if: | github.event_name != 'pull_request_target' && (inputs.testflight_distribute || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop') + env: + _APP_STORE_CONNECT_TEAM_ISSUER: ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-TEAM-ISSUER }} run: | brew install gsed @@ -1652,7 +1668,7 @@ jobs: cat << EOF > ~/secrets/appstoreconnect-fastlane.json { - "issuer_id": "${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-TEAM-ISSUER }}", + "issuer_id": "$_APP_STORE_CONNECT_TEAM_ISSUER", "key_id": "6TV9MKN3GP", "key": "$KEY_WITHOUT_NEWLINES" } @@ -1671,14 +1687,14 @@ jobs: GIT_CHANGE="$(git show -s --format=%s)" - BRANCH=$(echo $BRANCH | sed 's/refs\/heads\///') + BRANCH=$(echo "$BRANCH" | sed 's/refs\/heads\///') CHANGELOG="$BRANCH: $GIT_CHANGE" fastlane pilot upload \ --app_identifier "com.bitwarden.desktop" \ --changelog "$CHANGELOG" \ - --api_key_path $HOME/secrets/appstoreconnect-fastlane.json \ + --api_key_path "$HOME/secrets/appstoreconnect-fastlane.json" \ --pkg "$(find ./dist/mas-universal/Bitwarden*.pkg)" - name: Post message to a Slack channel @@ -1724,6 +1740,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Log in to Azure uses: bitwarden/gh-actions/azure-login@main diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index d623f08ebac..6733eeca1b4 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -67,23 +67,24 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Get GitHub sha as version id: version - run: echo "value=${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT + run: echo "value=${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT" - name: Get Node Version id: retrieve-node-version run: | NODE_NVMRC=$(cat .nvmrc) NODE_VERSION=${NODE_NVMRC/v/''} - echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT + echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT" - name: Check secrets id: check-secrets run: | has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }} - echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT + echo "has_secrets=$has_secrets" >> "$GITHUB_OUTPUT" build-containers: @@ -137,6 +138,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Get Latest Server Version id: latest-server-version @@ -147,8 +149,10 @@ jobs: - name: Set Server Ref id: set-server-ref + env: + _SERVER_VERSION: ${{ steps.latest-server-version.outputs.version }} run: | - SERVER_REF="${{ steps.latest-server-version.outputs.version }}" + SERVER_REF="$_SERVER_VERSION" echo "Latest server release version: $SERVER_REF" if [[ "$GITHUB_REF" == "refs/heads/main" ]]; then SERVER_REF="$GITHUB_REF" @@ -158,7 +162,7 @@ jobs: SERVER_REF="refs/heads/main" fi echo "Server ref: $SERVER_REF" - echo "server_ref=$SERVER_REF" >> $GITHUB_OUTPUT + echo "server_ref=$SERVER_REF" >> "$GITHUB_OUTPUT" - name: Check out Server repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -166,18 +170,19 @@ jobs: path: server repository: bitwarden/server ref: ${{ steps.set-server-ref.outputs.server_ref }} + persist-credentials: false - name: Check Branch to Publish env: PUBLISH_BRANCHES: "main,rc,hotfix-rc-web" id: publish-branch-check run: | - IFS="," read -a publish_branches <<< $PUBLISH_BRANCHES + IFS="," read -a publish_branches <<< "$PUBLISH_BRANCHES" if [[ " ${publish_branches[*]} " =~ " ${GITHUB_REF:11} " ]]; then - echo "is_publish_branch=true" >> $GITHUB_ENV + echo "is_publish_branch=true" >> "$GITHUB_ENV" else - echo "is_publish_branch=false" >> $GITHUB_ENV + echo "is_publish_branch=false" >> "$GITHUB_ENV" fi - name: Add Git metadata to build version @@ -217,11 +222,13 @@ jobs: - name: Log into Prod container registry if: ${{ needs.setup.outputs.has_secrets == 'true' }} - run: az acr login -n ${_AZ_REGISTRY%.azurecr.io} + run: az acr login -n "${_AZ_REGISTRY%.azurecr.io}" ########## Generate image tag and build Docker image ########## - name: Generate container image tag id: tag + env: + _TAG_EXTENSION: ${{ github.event.inputs.custom_tag_extension }} run: | if [[ "${GITHUB_EVENT_NAME}" == "pull_request" || "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize branch name to alphanumeric only @@ -231,7 +238,7 @@ jobs: if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then SANITIZED_REPO_NAME=$(echo "$_GITHUB_PR_REPO_NAME" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize repo name to alphanumeric only - IMAGE_TAG=$SANITIZED_REPO_NAME-$IMAGE_TAG # Add repo name to the tag + IMAGE_TAG="$SANITIZED_REPO_NAME-$IMAGE_TAG" # Add repo name to the tag IMAGE_TAG=${IMAGE_TAG:0:128} # Limit to 128 characters, as that's the max length for Docker image tags fi @@ -239,13 +246,13 @@ jobs: IMAGE_TAG=dev fi - TAG_EXTENSION=${{ github.event.inputs.custom_tag_extension }} + TAG_EXTENSION="$_TAG_EXTENSION" if [[ $TAG_EXTENSION ]]; then - IMAGE_TAG=$IMAGE_TAG-$TAG_EXTENSION + IMAGE_TAG="$IMAGE_TAG-$TAG_EXTENSION" fi - echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT + echo "image_tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT" ########## Build Image ########## - name: Generate image full name @@ -253,7 +260,7 @@ jobs: env: IMAGE_TAG: ${{ steps.tag.outputs.image_tag }} PROJECT_NAME: ${{ matrix.image_name }} - run: echo "name=$_AZ_REGISTRY/${PROJECT_NAME}:${IMAGE_TAG}" >> $GITHUB_OUTPUT + run: echo "name=$_AZ_REGISTRY/${PROJECT_NAME}:${IMAGE_TAG}" >> "$GITHUB_OUTPUT" - name: Build Docker image id: build-container @@ -276,7 +283,7 @@ jobs: if: ${{ needs.setup.outputs.has_secrets == 'true' }} env: IMAGE_NAME: ${{ steps.image-name.outputs.name }} - run: docker push $IMAGE_NAME + run: docker push "$IMAGE_NAME" - name: Zip project working-directory: apps/web @@ -284,10 +291,10 @@ jobs: IMAGE_NAME: ${{ steps.image-name.outputs.name }} run: | mkdir build - docker run --rm --volume $(pwd)/build:/temp --entrypoint sh \ - $IMAGE_NAME -c "cp -r ./ /temp" + docker run --rm --volume "$(pwd)/build":/temp --entrypoint sh \ + "$IMAGE_NAME" -c "cp -r ./ /temp" - zip -r web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip build + zip -r web-$_VERSION-${{ matrix.artifact_name }}.zip build - name: Upload ${{ matrix.artifact_name }} artifact uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 @@ -306,12 +313,13 @@ jobs: DIGEST: ${{ steps.build-container.outputs.digest }} TAGS: ${{ steps.image-name.outputs.name }} run: | - IFS="," read -a tags <<< "${TAGS}" - images="" - for tag in "${tags[@]}"; do - images+="${tag}@${DIGEST} " + IFS=',' read -r -a tags_array <<< "${TAGS}" + images=() + for tag in "${tags_array[@]}"; do + images+=("${tag}@${DIGEST}") done - cosign sign --yes ${images} + cosign sign --yes "${images[@]}" + echo "images=${images[*]}" >> "$GITHUB_OUTPUT" - name: Scan Docker image if: ${{ needs.setup.outputs.has_secrets == 'true' }} @@ -324,14 +332,14 @@ jobs: - name: Upload Grype results to GitHub if: ${{ needs.setup.outputs.has_secrets == 'true' }} - uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 + uses: github/codeql-action/upload-sarif@573acd9552f33577783abde4acb66a1058e762e5 # codeql-bundle-v2.23.1 with: sarif_file: ${{ steps.container-scan.outputs.sarif }} sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }} ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }} - name: Log out of Docker - run: docker logout $_AZ_REGISTRY + run: docker logout "$_AZ_REGISTRY" - name: Log out from Azure if: ${{ needs.setup.outputs.has_secrets == 'true' }} @@ -352,6 +360,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Log in to Azure uses: bitwarden/gh-actions/azure-login@main diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 2b7b6394f24..133f5b730b8 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -35,6 +35,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 + persist-credentials: false - name: Get changed files id: get-changed-files-for-chromatic @@ -54,7 +55,7 @@ jobs: run: | NODE_NVMRC=$(cat .nvmrc) NODE_VERSION=${NODE_NVMRC/v/''} - echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT + echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT" - name: Set up Node uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 diff --git a/.github/workflows/crowdin-pull.yml b/.github/workflows/crowdin-pull.yml index 0b891203855..3be294145ec 100644 --- a/.github/workflows/crowdin-pull.yml +++ b/.github/workflows/crowdin-pull.yml @@ -59,6 +59,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: token: ${{ steps.app-token.outputs.token }} + persist-credentials: false - name: Download translations uses: bitwarden/gh-actions/crowdin@main diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index d3788dc77b9..26a83e89773 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -74,56 +74,58 @@ jobs: steps: - name: Configure id: config + env: + _ENVIRONMENT: ${{ inputs.environment }} run: | - ENV_NAME_LOWER=$(echo "${{ inputs.environment }}" | awk '{print tolower($0)}') - echo "configuring the Web deploy for ${{ inputs.environment }}" - echo "environment=${{ inputs.environment }}" >> $GITHUB_OUTPUT + ENV_NAME_LOWER=$(echo "$_ENVIRONMENT" | awk '{print tolower($0)}') + echo "configuring the Web deploy for _ENVIRONMENT" + echo "environment=$_ENVIRONMENT" >> "$GITHUB_OUTPUT" - case ${{ inputs.environment }} in + case $_ENVIRONMENT in "USQA") - echo "azure_login_client_key_name=AZURE_CLIENT_ID_USQA" >> $GITHUB_OUTPUT - echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_USQA" >> $GITHUB_OUTPUT - echo "retrieve_secrets_keyvault=bw-webvault-rlktusqa-kv" >> $GITHUB_OUTPUT - echo "environment_artifact=web-*-cloud-QA.zip" >> $GITHUB_OUTPUT - echo "environment_name=Web Vault - US QA Cloud" >> $GITHUB_OUTPUT - echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT - echo "slack_channel_name=alerts-deploy-qa" >> $GITHUB_OUTPUT + echo "azure_login_client_key_name=AZURE_CLIENT_ID_USQA" >> "$GITHUB_OUTPUT" + echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_USQA" >> "$GITHUB_OUTPUT" + echo "retrieve_secrets_keyvault=bw-webvault-rlktusqa-kv" >> "$GITHUB_OUTPUT" + echo "environment_artifact=web-*-cloud-QA.zip" >> "$GITHUB_OUTPUT" + echo "environment_name=Web Vault - US QA Cloud" >> "$GITHUB_OUTPUT" + echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> "$GITHUB_OUTPUT" + echo "slack_channel_name=alerts-deploy-qa" >> "$GITHUB_OUTPUT" ;; "EUQA") - echo "azure_login_client_key_name=AZURE_CLIENT_ID_EUQA" >> $GITHUB_OUTPUT - echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_EUQA" >> $GITHUB_OUTPUT - echo "retrieve_secrets_keyvault=webvaulteu-westeurope-qa" >> $GITHUB_OUTPUT - echo "environment_artifact=web-*-cloud-euqa.zip" >> $GITHUB_OUTPUT - echo "environment_name=Web Vault - EU QA Cloud" >> $GITHUB_OUTPUT - echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT - echo "slack_channel_name=alerts-deploy-qa" >> $GITHUB_OUTPUT + echo "azure_login_client_key_name=AZURE_CLIENT_ID_EUQA" >> "$GITHUB_OUTPUT" + echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_EUQA" >> "$GITHUB_OUTPUT" + echo "retrieve_secrets_keyvault=webvaulteu-westeurope-qa" >> "$GITHUB_OUTPUT" + echo "environment_artifact=web-*-cloud-euqa.zip" >> "$GITHUB_OUTPUT" + echo "environment_name=Web Vault - EU QA Cloud" >> "$GITHUB_OUTPUT" + echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> "$GITHUB_OUTPUT" + echo "slack_channel_name=alerts-deploy-qa" >> "$GITHUB_OUTPUT" ;; "USPROD") - echo "azure_login_client_key_name=AZURE_CLIENT_ID_USPROD" >> $GITHUB_OUTPUT - echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_USPROD" >> $GITHUB_OUTPUT - echo "retrieve_secrets_keyvault=bw-webvault-klrt-kv" >> $GITHUB_OUTPUT - echo "environment_artifact=web-*-cloud-COMMERCIAL.zip" >> $GITHUB_OUTPUT - echo "environment_name=Web Vault - US Production Cloud" >> $GITHUB_OUTPUT - echo "environment_url=http://vault.bitwarden.com" >> $GITHUB_OUTPUT - echo "slack_channel_name=alerts-deploy-prd" >> $GITHUB_OUTPUT + echo "azure_login_client_key_name=AZURE_CLIENT_ID_USPROD" >> "$GITHUB_OUTPUT" + echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_USPROD" >> "$GITHUB_OUTPUT" + echo "retrieve_secrets_keyvault=bw-webvault-klrt-kv" >> "$GITHUB_OUTPUT" + echo "environment_artifact=web-*-cloud-COMMERCIAL.zip" >> "$GITHUB_OUTPUT" + echo "environment_name=Web Vault - US Production Cloud" >> "$GITHUB_OUTPUT" + echo "environment_url=http://vault.bitwarden.com" >> "$GITHUB_OUTPUT" + echo "slack_channel_name=alerts-deploy-prd" >> "$GITHUB_OUTPUT" ;; "EUPROD") - echo "azure_login_client_key_name=AZURE_CLIENT_ID_EUPROD" >> $GITHUB_OUTPUT - echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_EUPROD" >> $GITHUB_OUTPUT - echo "retrieve_secrets_keyvault=webvault-westeurope-prod" >> $GITHUB_OUTPUT - echo "environment_artifact=web-*-cloud-euprd.zip" >> $GITHUB_OUTPUT - echo "environment_name=Web Vault - EU Production Cloud" >> $GITHUB_OUTPUT - echo "environment_url=http://vault.bitwarden.eu" >> $GITHUB_OUTPUT - echo "slack_channel_name=alerts-deploy-prd" >> $GITHUB_OUTPUT + echo "azure_login_client_key_name=AZURE_CLIENT_ID_EUPROD" >> "$GITHUB_OUTPUT" + echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_EUPROD" >> "$GITHUB_OUTPUT" + echo "retrieve_secrets_keyvault=webvault-westeurope-prod" >> "$GITHUB_OUTPUT" + echo "environment_artifact=web-*-cloud-euprd.zip" >> "$GITHUB_OUTPUT" + echo "environment_name=Web Vault - EU Production Cloud" >> "$GITHUB_OUTPUT" + echo "environment_url=http://vault.bitwarden.eu" >> "$GITHUB_OUTPUT" + echo "slack_channel_name=alerts-deploy-prd" >> "$GITHUB_OUTPUT" ;; "USDEV") - echo "azure_login_client_key_name=AZURE_CLIENT_ID_USDEV" >> $GITHUB_OUTPUT - echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_USDEV" >> $GITHUB_OUTPUT - echo "retrieve_secrets_keyvault=webvault-eastus-dev" >> $GITHUB_OUTPUT - echo "environment_artifact=web-*-cloud-usdev.zip" >> $GITHUB_OUTPUT - echo "environment_name=Web Vault - US Development Cloud" >> $GITHUB_OUTPUT - echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT - echo "slack_channel_name=alerts-deploy-dev" >> $GITHUB_OUTPUT + echo "azure_login_client_key_name=AZURE_CLIENT_ID_USDEV" >> "$GITHUB_OUTPUT" + echo "azure_login_subscription_id_key_name=AZURE_SUBSCRIPTION_ID_USDEV" >> "$GITHUB_OUTPUT" + echo "retrieve_secrets_keyvault=webvault-eastus-dev" >> "$GITHUB_OUTPUT" + echo "environment_artifact=web-*-cloud-usdev.zip" >> "$GITHUB_OUTPUT" + echo "environment_name=Web Vault - US Development Cloud" >> "$GITHUB_OUTPUT" + echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> "$GITHUB_OUTPUT" + echo "slack_channel_name=alerts-deploy-dev" >> "$GITHUB_OUTPUT" ;; esac @@ -131,12 +133,14 @@ jobs: env: BUILD_WEB_RUN_ID: ${{ inputs.build-web-run-id }} GH_TOKEN: ${{ github.token }} + _ENVIRONMENT: ${{ inputs.environment }} + _BRANCH_OR_TAG: ${{ inputs.branch-or-tag }} run: | BRANCH_OR_TAG_LOWER="" if [[ "$BUILD_WEB_RUN_ID" == "" ]]; then - BRANCH_OR_TAG_LOWER=$(echo ${{ inputs.branch-or-tag }} | awk '{print tolower($0)}') + BRANCH_OR_TAG_LOWER=$(echo "$_BRANCH_OR_TAG" | awk '{print tolower($0)}') else - BRANCH_OR_TAG_LOWER=$(gh api /repos/bitwarden/clients/actions/runs/$BUILD_WEB_RUN_ID/artifacts --jq '.artifacts[0].workflow_run.head_branch' | awk '{print tolower($0)}') + BRANCH_OR_TAG_LOWER=$(gh api "/repos/bitwarden/clients/actions/runs/$BUILD_WEB_RUN_ID/artifacts" --jq '.artifacts[0].workflow_run.head_branch' | awk '{print tolower($0)}') fi echo "Branch/Tag: $BRANCH_OR_TAG_LOWER" @@ -151,23 +155,23 @@ jobs: DEV_ALLOWED_TAGS_PATTERN='main' if [[ \ - ${{ inputs.environment }} =~ \.*($PROD_ENV_PATTERN)\.* && \ + $_ENVIRONMENT =~ \.*($PROD_ENV_PATTERN)\.* && \ ! "$BRANCH_OR_TAG_LOWER" =~ ^($PROD_ALLOWED_TAGS_PATTERN).* \ ]] || [[ \ - ${{ inputs.environment }} =~ \.*($QA_ENV_PATTERN)\.* && \ + $_ENVIRONMENT =~ \.*($QA_ENV_PATTERN)\.* && \ ! "$BRANCH_OR_TAG_LOWER" =~ ^($QA_ALLOWED_TAGS_PATTERN).* \ ]] || [[ \ - ${{ inputs.environment }} =~ \.*($DEV_ENV_PATTERN)\.* && \ - $BRANCH_OR_TAG_LOWER != $DEV_ALLOWED_TAGS_PATTERN \ + $_ENVIRONMENT =~ \.*($DEV_ENV_PATTERN)\.* && \ + $BRANCH_OR_TAG_LOWER != "$DEV_ALLOWED_TAGS_PATTERN" \ ]]; then echo "!Deployment blocked!" - echo "Attempting to deploy a tag that is not allowed in ${{ inputs.environment }} environment" + echo "Attempting to deploy a tag that is not allowed in $_ENVIRONMENT environment" echo - echo "Environment: ${{ inputs.environment }}" + echo "Environment: $_ENVIRONMENT" echo "Tag: $BRANCH_OR_TAG_LOWER" exit 1 else - echo "The input Branch/Tag: '$BRANCH_OR_TAG_LOWER' is allowed to deploy on ${{ inputs.environment }} environment" + echo "The input Branch/Tag: '$BRANCH_OR_TAG_LOWER' is allowed to deploy on $_ENVIRONMENT environment" fi approval: @@ -251,19 +255,24 @@ jobs: id: set-artifact-commit env: GH_TOKEN: ${{ github.token }} + _BUILD_WEB_RUN_ID: ${{ inputs.build-web-run-id }} + _ARTIFACT_BUILD_COMMIT: ${{ steps.download-latest-artifacts-run-id.outputs.artifact-build-commit }} + _DOWNLOAD_LATEST_ARTIFACTS_OUTCOME: ${{ steps.download-latest-artifacts.outcome }} + _WORKFLOW_ID: ${{ steps.trigger-build-web.outputs.workflow_id}} + _ARTIFACT_COMMIT: ${{ steps.download-latest-artifacts.outputs.artifact-build-commit }} run: | # If run-id was used, get the commit from the download-latest-artifacts-run-id step - if [ "${{ inputs.build-web-run-id }}" ]; then - echo "commit=${{ steps.download-latest-artifacts-run-id.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT + if [ "$_BUILD_WEB_RUN_ID" ]; then + echo "commit=$_ARTIFACT_BUILD_COMMIT" >> "$GITHUB_OUTPUT" - elif [ "${{ steps.download-latest-artifacts.outcome }}" == "failure" ]; then + elif [ "$_DOWNLOAD_LATEST_ARTIFACTS_OUTCOME" == "failure" ]; then # If the download-latest-artifacts step failed, query the GH API to get the commit SHA of the artifact that was just built with trigger-build-web. - commit=$(gh api /repos/bitwarden/clients/actions/runs/${{ steps.trigger-build-web.outputs.workflow_id }}/artifacts --jq '.artifacts[0].workflow_run.head_sha') - echo "commit=$commit" >> $GITHUB_OUTPUT + commit=$(gh api "/repos/bitwarden/clients/actions/runs/$_WORKFLOW_ID/artifacts" --jq '.artifacts[0].workflow_run.head_sha') + echo "commit=$commit" >> "$GITHUB_OUTPUT" else # Set the commit to the output of step download-latest-artifacts. - echo "commit=${{ steps.download-latest-artifacts.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT + echo "commit=$_ARTIFACT_COMMIT" >> "$GITHUB_OUTPUT" fi notify-start: @@ -299,12 +308,14 @@ jobs: name: Display commit needs: artifact-check runs-on: ubuntu-22.04 + env: + _ARTIFACT_BUILD_COMMIT_SHA: ${{ needs.artifact-check.outputs.artifact_build_commit }} steps: - name: Display commit SHA run: | REPO_URL="https://github.com/bitwarden/clients/commit" - COMMIT_SHA="${{ needs.artifact-check.outputs.artifact_build_commit }}" - echo ":steam_locomotive: View [commit]($REPO_URL/$COMMIT_SHA)" >> $GITHUB_STEP_SUMMARY + COMMIT_SHA="$_ARTIFACT_BUILD_COMMIT_SHA" + echo ":steam_locomotive: View [commit]($REPO_URL/$COMMIT_SHA)" >> "$GITHUB_STEP_SUMMARY" azure-deploy: name: Deploy Web Vault to ${{ inputs.environment }} Storage Account @@ -358,7 +369,7 @@ jobs: - name: Unzip build asset working-directory: apps/web - run: unzip ${{ env._ENVIRONMENT_ARTIFACT }} + run: unzip "$_ENVIRONMENT_ARTIFACT" - name: Login to Azure uses: bitwarden/gh-actions/azure-login@main @@ -379,9 +390,10 @@ jobs: env: AZCOPY_AUTO_LOGIN_TYPE: AZCLI AZCOPY_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + _VAULT_NAME: ${{ steps.retrieve-secrets-azcopy.outputs.sa-bitwarden-web-vault-name }}. run: | - azcopy sync ./build 'https://${{ steps.retrieve-secrets-azcopy.outputs.sa-bitwarden-web-vault-name }}.blob.core.windows.net/$web/' \ - --delete-destination=${{ inputs.force-delete-destination }} --compare-hash="MD5" + azcopy sync ./build "https://$_VAULT_NAME.blob.core.windows.net/$web/" \ + --delete-destination="${{ inputs.force-delete-destination }}" --compare-hash="MD5" - name: Log out from Azure uses: bitwarden/gh-actions/azure-logout@main diff --git a/.github/workflows/lint-crowdin-config.yml b/.github/workflows/lint-crowdin-config.yml index 38a3ef59ea7..40f73f7fc5a 100644 --- a/.github/workflows/lint-crowdin-config.yml +++ b/.github/workflows/lint-crowdin-config.yml @@ -25,6 +25,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 + persist-credentials: false - name: Log in to Azure uses: bitwarden/gh-actions/azure-login@main diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 14b5d51d9ef..0136bd2f70f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -32,6 +32,8 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Lint filenames (no capital characters) run: | @@ -58,7 +60,7 @@ jobs: run: | NODE_NVMRC=$(cat .nvmrc) NODE_VERSION=${NODE_NVMRC/v/''} - echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT + echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT" - name: Set up Node uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 @@ -90,6 +92,8 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Check Rust version run: rustup --version diff --git a/.github/workflows/locales-lint.yml b/.github/workflows/locales-lint.yml index 0c8148d4c28..26c910f955e 100644 --- a/.github/workflows/locales-lint.yml +++ b/.github/workflows/locales-lint.yml @@ -18,17 +18,19 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Checkout base branch repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.base.sha }} path: base + persist-credentials: false - name: Install dependencies run: npm ci - name: Compare run: | - npm run test:locales - if [ $? -eq 0 ]; then + if npm run test:locales; then echo "Lint check successful." else echo "Lint check failed." diff --git a/.github/workflows/nx.yml b/.github/workflows/nx.yml index 526c2b5d864..3e14169a065 100644 --- a/.github/workflows/nx.yml +++ b/.github/workflows/nx.yml @@ -15,6 +15,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - name: Get Node Version id: retrieve-node-version @@ -22,7 +23,7 @@ jobs: run: | NODE_NVMRC=$(cat .nvmrc) NODE_VERSION=${NODE_NVMRC/v/''} - echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT + echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT" - name: Set up Node uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 1287970ccf6..9bbd982d32f 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -65,14 +65,16 @@ jobs: - name: Version output id: version-output + env: + _INPUT_VERSION: ${{ inputs.version }} run: | - if [[ "${{ inputs.version }}" == "latest" || "${{ inputs.version }}" == "" ]]; then + if [[ "$_INPUT_VERSION" == "latest" || "$_INPUT_VERSION" == "" ]]; then VERSION=$(curl "https://api.github.com/repos/bitwarden/clients/releases" | jq -c '.[] | select(.tag_name | contains("cli")) | .tag_name' | head -1 | grep -ohE '20[0-9]{2}\.([1-9]|1[0-2])\.[0-9]+') echo "Latest Released Version: $VERSION" - echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> "$GITHUB_OUTPUT" else - echo "Release Version: ${{ inputs.version }}" - echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + echo "Release Version: $_INPUT_VERSION" + echo "version=$_INPUT_VERSION" >> "$GITHUB_OUTPUT" fi - name: Create GitHub deployment @@ -100,6 +102,8 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Log in to Azure uses: bitwarden/gh-actions/azure-login@main @@ -122,14 +126,14 @@ jobs: uses: samuelmeuli/action-snapcraft@fceeb3c308e76f3487e72ef608618de625fb7fe8 # v3.0.1 - name: Download artifacts - run: wget https://github.com/bitwarden/clients/releases/download/cli-v${{ env._PKG_VERSION }}/bw_${{ env._PKG_VERSION }}_amd64.snap + run: wget "https://github.com/bitwarden/clients/releases/download/cli-v$_PKG_VERSION/bw_$_PKG_VERSION_amd64.snap" - name: Publish Snap & logout if: ${{ inputs.publish_type != 'Dry Run' }} env: SNAPCRAFT_STORE_CREDENTIALS: ${{ steps.retrieve-secrets.outputs.snapcraft-store-token }} run: | - snapcraft upload bw_${{ env._PKG_VERSION }}_amd64.snap --release stable + snapcraft upload "bw_$_PKG_VERSION_amd64.snap" --release stable snapcraft logout choco: @@ -146,6 +150,8 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Log in to Azure uses: bitwarden/gh-actions/azure-login@main @@ -173,7 +179,7 @@ jobs: run: New-Item -ItemType directory -Path ./dist - name: Download artifacts - run: Invoke-WebRequest -Uri "https://github.com/bitwarden/clients/releases/download/cli-v${{ env._PKG_VERSION }}/bitwarden-cli.${{ env._PKG_VERSION }}.nupkg" -OutFile bitwarden-cli.${{ env._PKG_VERSION }}.nupkg + run: Invoke-WebRequest -Uri "https://github.com/bitwarden/clients/releases/download/cli-v$_PKG_VERSION/bitwarden-cli.$_PKG_VERSION.nupkg" -OutFile bitwarden-cli.$_PKG_VERSION.nupkg working-directory: apps/cli/dist - name: Push to Chocolatey @@ -196,6 +202,8 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Get Node version id: retrieve-node-version @@ -203,7 +211,7 @@ jobs: run: | NODE_NVMRC=$(cat .nvmrc) NODE_VERSION=${NODE_NVMRC/v/''} - echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT + echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT" - name: Set up Node uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 @@ -219,8 +227,8 @@ jobs: - name: Download and set up artifact run: | mkdir -p build - wget https://github.com/bitwarden/clients/releases/download/cli-v${{ env._PKG_VERSION }}/bitwarden-cli-${{ env._PKG_VERSION }}-npm-build.zip - unzip bitwarden-cli-${{ env._PKG_VERSION }}-npm-build.zip -d build + wget "https://github.com/bitwarden/clients/releases/download/cli-v$_PKG_VERSION/bitwarden-cli-$_PKG_VERSION-npm-build.zip" + unzip "bitwarden-cli-$_PKG_VERSION-npm-build.zip" -d build - name: Publish NPM if: ${{ inputs.publish_type != 'Dry Run' }} diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index f0de331431c..a747012467e 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -72,39 +72,47 @@ jobs: - name: Check Publish Version id: version + env: + _INPUT_VERSION: ${{ inputs.version }} run: | - if [[ "${{ inputs.version }}" == "latest" || "${{ inputs.version }}" == "" ]]; then - TAG_NAME=$(curl "https://api.github.com/repos/bitwarden/clients/releases" | jq -c '.[] | select(.tag_name | contains("desktop")) | .tag_name' | head -1 | cut -d '"' -f 2) - VERSION=$(echo $TAG_NAME | sed "s/desktop-v//") + if [[ "$_INPUT_VERSION" == "latest" || "$_INPUT_VERSION" == "" ]]; then + TAG_NAME=$(curl "https://api.github.com/repos/bitwarden/clients/releases" \ + | jq -c '.[] | select(.tag_name | contains("desktop")) | .tag_name' \ + | head -1 | cut -d '"' -f 2) + VERSION="${TAG_NAME#desktop-v}" + echo "Latest Released Version: $VERSION" - echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "Tag name: $TAG_NAME" - echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT + echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT" else - echo "Release Version: ${{ inputs.version }}" - echo "version=${{ inputs.version }}" + VERSION="$_INPUT_VERSION" + TAG_NAME="desktop-v$VERSION" - TAG_NAME="desktop-v${{ inputs.version }}" + echo "Release Version: $VERSION" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "Tag name: $TAG_NAME" - echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT + echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT" fi - name: Get Version Channel id: release_channel + env: + _VERSION: ${{ steps.version.outputs.version }} run: | - case "${{ steps.version.outputs.version }}" in + case "${_VERSION}" in *"alpha"*) - echo "channel=alpha" >> $GITHUB_OUTPUT + echo "channel=alpha" >> "$GITHUB_OUTPUT" echo "[!] We do not yet support 'alpha'" exit 1 ;; *"beta"*) - echo "channel=beta" >> $GITHUB_OUTPUT + echo "channel=beta" >> "$GITHUB_OUTPUT" ;; *) - echo "channel=latest" >> $GITHUB_OUTPUT + echo "channel=latest" >> "$GITHUB_OUTPUT" ;; esac @@ -159,16 +167,16 @@ jobs: env: GH_TOKEN: ${{ github.token }} working-directory: apps/desktop/artifacts - run: gh release download ${{ env._RELEASE_TAG }} -R bitwarden/clients + run: gh release download "$_RELEASE_TAG" -R bitwarden/clients - name: Set staged rollout percentage env: RELEASE_CHANNEL: ${{ needs.setup.outputs.release_channel }} ROLLOUT_PCT: ${{ inputs.electron_rollout_percentage }} run: | - echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}.yml - echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}-linux.yml - echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}-mac.yml + echo "stagingPercentage: ${ROLLOUT_PCT}" >> "apps/desktop/artifacts/${RELEASE_CHANNEL}.yml" + echo "stagingPercentage: ${ROLLOUT_PCT}" >> "apps/desktop/artifacts/${RELEASE_CHANNEL}-linux.yml" + echo "stagingPercentage: ${ROLLOUT_PCT}" >> "apps/desktop/artifacts/${RELEASE_CHANNEL}-mac.yml" - name: Publish artifacts to S3 if: ${{ inputs.publish_type != 'Dry Run' }} @@ -179,7 +187,7 @@ jobs: AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.aws-electron-bucket-name }} working-directory: apps/desktop/artifacts run: | - aws s3 cp ./ $AWS_S3_BUCKET_NAME/desktop/ \ + aws s3 cp ./ "$AWS_S3_BUCKET_NAME/desktop/" \ --acl "public-read" \ --recursive \ --quiet @@ -214,6 +222,8 @@ jobs: steps: - name: Checkout Repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Log in to Azure uses: bitwarden/gh-actions/azure-login@main @@ -241,14 +251,14 @@ jobs: - name: Download artifacts working-directory: apps/desktop/dist - run: wget https://github.com/bitwarden/clients/releases/download/${{ env._RELEASE_TAG }}/bitwarden_${{ env._PKG_VERSION }}_amd64.snap + run: wget "https://github.com/bitwarden/clients/releases/download/$_RELEASE_TAG/bitwarden_$_PKG_VERSION_amd64.snap" - name: Deploy to Snap Store if: ${{ inputs.publish_type != 'Dry Run' }} env: SNAPCRAFT_STORE_CREDENTIALS: ${{ steps.retrieve-secrets.outputs.snapcraft-store-token }} run: | - snapcraft upload bitwarden_${{ env._PKG_VERSION }}_amd64.snap --release stable + snapcraft upload "bitwarden_$_PKG_VERSION_amd64.snap" --release stable snapcraft logout working-directory: apps/desktop/dist @@ -266,6 +276,8 @@ jobs: steps: - name: Checkout Repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Print Environment run: | @@ -300,7 +312,7 @@ jobs: - name: Download artifacts working-directory: apps/desktop/dist - run: Invoke-WebRequest -Uri "https://github.com/bitwarden/clients/releases/download/${{ env._RELEASE_TAG }}/bitwarden.${{ env._PKG_VERSION }}.nupkg" -OutFile bitwarden.${{ env._PKG_VERSION }}.nupkg + run: Invoke-WebRequest -Uri "https://github.com/bitwarden/clients/releases/download/$_RELEASE_TAG/bitwarden.$_PKG_VERSION.nupkg" -OutFile "bitwarden.$_PKG_VERSION.nupkg" - name: Push to Chocolatey if: ${{ inputs.publish_type != 'Dry Run' }} @@ -321,6 +333,8 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Validate release notes for MAS if: inputs.mas_publish && (inputs.release_notes == '' || inputs.release_notes == null) @@ -331,7 +345,7 @@ jobs: - name: Download MacOS App Store build number working-directory: apps/desktop - run: wget https://github.com/bitwarden/clients/releases/download/${{ env._RELEASE_TAG }}/macos-build-number.json + run: wget "https://github.com/bitwarden/clients/releases/download/$_RELEASE_TAG/macos-build-number.json" - name: Setup Ruby and Install Fastlane uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0 @@ -365,12 +379,14 @@ jobs: env: APP_STORE_CONNECT_TEAM_ISSUER: ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-TEAM-ISSUER }} APP_STORE_CONNECT_AUTH_KEY: ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-AUTH-KEY }} + _RELEASE_NOTES: ${{ inputs.release_notes }} + _PUBLISH_TYPE: ${{ inputs.publish_type }} working-directory: apps/desktop run: | BUILD_NUMBER=$(jq -r '.buildNumber' macos-build-number.json) - CHANGELOG="${{ inputs.release_notes }}" - IS_DRY_RUN="${{ inputs.publish_type == 'Dry Run' }}" - + CHANGELOG="$_RELEASE_NOTES" + IS_DRY_RUN="$_PUBLISH_TYPE == 'Dry Run'" + if [ "$IS_DRY_RUN" = "true" ]; then echo "🧪 DRY RUN MODE - Testing without actual App Store submission" echo "📦 Would publish build $BUILD_NUMBER to Mac App Store" @@ -388,10 +404,10 @@ jobs: fi fastlane publish --verbose \ - app_version:"${{ env._PKG_VERSION }}" \ - build_number:$BUILD_NUMBER \ + app_version:"$PKG_VERSION" \ + build_number:"$BUILD_NUMBER" \ changelog:"$CHANGELOG" \ - dry_run:$IS_DRY_RUN + dry_run:"$IS_DRY_RUN" update-deployment: name: Update Deployment Status diff --git a/.github/workflows/publish-web.yml b/.github/workflows/publish-web.yml index 6446e625156..9f9cbd5c58e 100644 --- a/.github/workflows/publish-web.yml +++ b/.github/workflows/publish-web.yml @@ -29,6 +29,8 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Branch check if: ${{ inputs.publish_type != 'Dry Run' }} @@ -73,6 +75,8 @@ jobs: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false ########## ACR ########## - name: Log in to Azure @@ -100,33 +104,33 @@ jobs: - name: Pull branch image run: | if [[ "${{ inputs.publish_type }}" == "Dry Run" ]]; then - docker pull $_AZ_REGISTRY/web:latest + docker pull "$_AZ_REGISTRY/web:latest" else - docker pull $_AZ_REGISTRY/web:$_BRANCH_NAME + docker pull "$_AZ_REGISTRY/web:$_BRANCH_NAME" fi - name: Tag version run: | if [[ "${{ inputs.publish_type }}" == "Dry Run" ]]; then - docker tag $_AZ_REGISTRY/web:latest $_AZ_REGISTRY/web:dryrun - docker tag $_AZ_REGISTRY/web:latest $_AZ_REGISTRY/web-sh:dryrun + docker tag "$_AZ_REGISTRY/web:latest" "$_AZ_REGISTRY/web:dryrun" + docker tag "$_AZ_REGISTRY/web:latest" "$_AZ_REGISTRY/web-sh:dryrun" else - docker tag $_AZ_REGISTRY/web:$_BRANCH_NAME $_AZ_REGISTRY/web:$_RELEASE_VERSION - docker tag $_AZ_REGISTRY/web:$_BRANCH_NAME $_AZ_REGISTRY/web-sh:$_RELEASE_VERSION - docker tag $_AZ_REGISTRY/web:$_BRANCH_NAME $_AZ_REGISTRY/web:latest - docker tag $_AZ_REGISTRY/web:$_BRANCH_NAME $_AZ_REGISTRY/web-sh:latest + docker tag "$_AZ_REGISTRY/web:$_BRANCH_NAME" "$_AZ_REGISTRY/web:$_RELEASE_VERSION" + docker tag "$_AZ_REGISTRY/web:$_BRANCH_NAME" "$_AZ_REGISTRY/web-sh:$_RELEASE_VERSION" + docker tag "$_AZ_REGISTRY/web:$_BRANCH_NAME" "$_AZ_REGISTRY/web:latest" + docker tag "$_AZ_REGISTRY/web:$_BRANCH_NAME" "$_AZ_REGISTRY/web-sh:latest" fi - name: Push version run: | if [[ "${{ inputs.publish_type }}" == "Dry Run" ]]; then - docker push $_AZ_REGISTRY/web:dryrun - docker push $_AZ_REGISTRY/web-sh:dryrun + docker push "$_AZ_REGISTRY/web:dryrun" + docker push "$_AZ_REGISTRY/web-sh:dryrun" else - docker push $_AZ_REGISTRY/web:$_RELEASE_VERSION - docker push $_AZ_REGISTRY/web-sh:$_RELEASE_VERSION - docker push $_AZ_REGISTRY/web:latest - docker push $_AZ_REGISTRY/web-sh:latest + docker push "$_AZ_REGISTRY/web:$_RELEASE_VERSION" + docker push "$_AZ_REGISTRY/web-sh:$_RELEASE_VERSION" + docker push "$_AZ_REGISTRY/web:latest" + docker push "$_AZ_REGISTRY/web-sh:latest" fi - name: Log out from Azure diff --git a/.github/workflows/release-browser.yml b/.github/workflows/release-browser.yml index ac79287f84d..a2fda230491 100644 --- a/.github/workflows/release-browser.yml +++ b/.github/workflows/release-browser.yml @@ -29,6 +29,8 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -60,6 +62,8 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Testing locales - extName length run: | @@ -69,9 +73,11 @@ jobs: echo "============" echo "extName string must be 40 characters or less" echo - for locale in $(ls src/_locales/); do - string_length=$(jq '.extName.message | length' src/_locales/$locale/messages.json) - if [[ $string_length -gt 40 ]]; then + + for locale_path in src/_locales/*/messages.json; do + locale=$(basename "$(dirname "$locale_path")") + string_length=$(jq '.extName.message | length' "$locale_path") + if [ "$string_length" -gt 40 ]; then echo "$locale: $string_length" found_error=true fi @@ -126,11 +132,11 @@ jobs: env: PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} run: | - mv browser-source.zip browser-source-$PACKAGE_VERSION.zip - mv dist-chrome.zip dist-chrome-$PACKAGE_VERSION.zip - mv dist-opera.zip dist-opera-$PACKAGE_VERSION.zip - mv dist-firefox.zip dist-firefox-$PACKAGE_VERSION.zip - mv dist-edge.zip dist-edge-$PACKAGE_VERSION.zip + mv browser-source.zip "browser-source-$PACKAGE_VERSION.zip" + mv dist-chrome.zip "dist-chrome-$PACKAGE_VERSION.zip" + mv dist-opera.zip "dist-opera-$PACKAGE_VERSION.zip" + mv dist-firefox.zip "dist-firefox-$PACKAGE_VERSION.zip" + mv dist-edge.zip "dist-edge-$PACKAGE_VERSION.zip" - name: Create release if: ${{ github.event.inputs.release_type != 'Dry Run' }} diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 2d7be2e186e..918f81e2723 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -30,6 +30,8 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Branch check if: ${{ inputs.release_type != 'Dry Run' }} diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index bfd6115a1a9..a97d72a32b0 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -32,6 +32,8 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -55,18 +57,20 @@ jobs: - name: Get Version Channel id: release_channel + env: + _VERSION: ${{ steps.version.outputs.version }} run: | - case "${{ steps.version.outputs.version }}" in + case "$_VERSION" in *"alpha"*) - echo "channel=alpha" >> $GITHUB_OUTPUT + echo "channel=alpha" >> "$GITHUB_OUTPUT" echo "[!] We do not yet support 'alpha'" exit 1 ;; *"beta"*) - echo "channel=beta" >> $GITHUB_OUTPUT + echo "channel=beta" >> "$GITHUB_OUTPUT" ;; *) - echo "channel=latest" >> $GITHUB_OUTPUT + echo "channel=latest" >> "$GITHUB_OUTPUT" ;; esac @@ -92,7 +96,7 @@ jobs: env: PKG_VERSION: ${{ steps.version.outputs.version }} working-directory: apps/desktop/artifacts - run: mv Bitwarden-${{ env.PKG_VERSION }}-universal.pkg Bitwarden-${{ env.PKG_VERSION }}-universal.pkg.archive + run: mv "Bitwarden-$PKG_VERSION-universal.pkg" "Bitwarden-$PKG_VERSION-universal.pkg.archive" - name: Create Release uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0 diff --git a/.github/workflows/release-web.yml b/.github/workflows/release-web.yml index 5a3c29d29fc..d616d7adb3f 100644 --- a/.github/workflows/release-web.yml +++ b/.github/workflows/release-web.yml @@ -26,6 +26,8 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -79,9 +81,11 @@ jobs: - name: Rename assets working-directory: apps/web/artifacts + env: + _RELEASE_VERSION: ${{ needs.setup.outputs.release_version }} run: | - mv web-*-selfhosted-COMMERCIAL.zip web-${{ needs.setup.outputs.release_version }}-selfhosted-COMMERCIAL.zip - mv web-*-selfhosted-open-source.zip web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip + mv web-*-selfhosted-COMMERCIAL.zip "web-$_RELEASE_VERSION-selfhosted-COMMERCIAL.zip" + mv web-*-selfhosted-open-source.zip "web-$_RELEASE_VERSION-selfhosted-open-source.zip" - name: Create release if: ${{ github.event.inputs.release_type != 'Dry Run' }} diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index ecb8e448a8a..acfda4cdb11 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -57,7 +57,7 @@ jobs: BRANCH="rc" fi - echo "branch=$BRANCH" >> $GITHUB_OUTPUT + echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" bump_version: name: Bump Version @@ -108,6 +108,7 @@ jobs: with: ref: main token: ${{ steps.app-token.outputs.token }} + persist-credentials: true - name: Configure Git run: | @@ -124,7 +125,7 @@ jobs: id: current-browser-version run: | CURRENT_VERSION=$(cat package.json | jq -r '.version') - echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT" working-directory: apps/browser - name: Browser - Verify input version @@ -140,8 +141,7 @@ jobs: fi # Check if version is newer. - printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V - if [ $? -eq 0 ]; then + if printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V; then echo "Version check successful." else echo "Version check failed." @@ -161,14 +161,14 @@ jobs: id: bump-browser-version-override env: VERSION: ${{ inputs.version_number_override }} - run: npm version --workspace=@bitwarden/browser $VERSION + run: npm version --workspace=@bitwarden/browser "$VERSION" - name: Bump Browser Version - Automatic Calculation if: ${{ inputs.bump_browser == true && inputs.version_number_override == '' }} id: bump-browser-version-automatic env: VERSION: ${{ steps.calculate-next-browser-version.outputs.version }} - run: npm version --workspace=@bitwarden/browser $VERSION + run: npm version --workspace=@bitwarden/browser "$VERSION" - name: Bump Browser Version - Manifest - Version Override if: ${{ inputs.bump_browser == true && inputs.version_number_override != '' }} @@ -211,7 +211,7 @@ jobs: id: current-cli-version run: | CURRENT_VERSION=$(cat package.json | jq -r '.version') - echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT" working-directory: apps/cli - name: CLI - Verify input version @@ -227,8 +227,7 @@ jobs: fi # Check if version is newer. - printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V - if [ $? -eq 0 ]; then + if printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V; then echo "Version check successful." else echo "Version check failed." @@ -248,14 +247,14 @@ jobs: id: bump-cli-version-override env: VERSION: ${{ inputs.version_number_override }} - run: npm version --workspace=@bitwarden/cli $VERSION + run: npm version --workspace=@bitwarden/cli "$VERSION" - name: Bump CLI Version - Automatic Calculation if: ${{ inputs.bump_cli == true && inputs.version_number_override == '' }} id: bump-cli-version-automatic env: VERSION: ${{ steps.calculate-next-cli-version.outputs.version }} - run: npm version --workspace=@bitwarden/cli $VERSION + run: npm version --workspace=@bitwarden/cli "$VERSION" ### Desktop - name: Get current Desktop version @@ -263,7 +262,7 @@ jobs: id: current-desktop-version run: | CURRENT_VERSION=$(cat package.json | jq -r '.version') - echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT" working-directory: apps/desktop - name: Desktop - Verify input version @@ -279,8 +278,7 @@ jobs: fi # Check if version is newer. - printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V - if [ $? -eq 0 ]; then + if printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V; then echo "Version check successful." else echo "Version check failed." @@ -300,27 +298,27 @@ jobs: id: bump-desktop-version-override env: VERSION: ${{ inputs.version_number_override }} - run: npm version --workspace=@bitwarden/desktop $VERSION + run: npm version --workspace=@bitwarden/desktop "$VERSION" - name: Bump Desktop Version - Root - Automatic Calculation if: ${{ inputs.bump_desktop == true && inputs.version_number_override == '' }} id: bump-desktop-version-automatic env: VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }} - run: npm version --workspace=@bitwarden/desktop $VERSION + run: npm version --workspace=@bitwarden/desktop "$VERSION" - name: Bump Desktop Version - App - Version Override if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }} env: VERSION: ${{ inputs.version_number_override }} - run: npm version $VERSION + run: npm version "$VERSION" working-directory: "apps/desktop/src" - name: Bump Desktop Version - App - Automatic Calculation if: ${{ inputs.bump_desktop == true && inputs.version_number_override == '' }} env: VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }} - run: npm version $VERSION + run: npm version "$VERSION" working-directory: "apps/desktop/src" ### Web @@ -329,7 +327,7 @@ jobs: id: current-web-version run: | CURRENT_VERSION=$(cat package.json | jq -r '.version') - echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT" working-directory: apps/web - name: Web - Verify input version @@ -345,8 +343,7 @@ jobs: fi # Check if version is newer. - printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V - if [ $? -eq 0 ]; then + if printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V; then echo "Version check successful." else echo "Version check failed." @@ -366,14 +363,14 @@ jobs: id: bump-web-version-override env: VERSION: ${{ inputs.version_number_override }} - run: npm version --workspace=@bitwarden/web-vault $VERSION + run: npm version --workspace=@bitwarden/web-vault "$VERSION" - name: Bump Web Version - Automatic Calculation if: ${{ inputs.bump_web == true && inputs.version_number_override == '' }} id: bump-web-version-automatic env: VERSION: ${{ steps.calculate-next-web-version.outputs.version }} - run: npm version --workspace=@bitwarden/web-vault $VERSION + run: npm version --workspace=@bitwarden/web-vault "$VERSION" ######################## @@ -381,38 +378,50 @@ jobs: id: set-final-version-output env: VERSION: ${{ inputs.version_number_override }} + _BUMP_BROWSER_VERSION_OVERRIDE_OUTCOME: ${{ steps.bump-browser-version-override.outcome }} + _BUMP_BROWSER_VERSION_AUTOMATIC_OUTCOME: ${{ steps.bump-browser-version-automatic.outcome }} + _CALCULATE_NEXT_BROWSER_VERSION: ${{ steps.calculate-next-browser-version.outputs.version }} + _BUMP_CLI_VERSION_OVERRIDE_OUTCOME: ${{ steps.bump-cli-version-override.outcome }} + _BUMP_CLI_VERSION_AUTOMATIC_OUTCOME: ${{ steps.bump-cli-version-automatic.outcome }} + _CALCULATE_NEXT_CLI_VERSION: ${{ steps.calculate-next-cli-version.outputs.version }} + _BUMP_DESKTOP_VERSION_OVERRIDE_OUTCOME: ${{ steps.bump-desktop-version-override.outcome }} + _BUMP_DESKTOP_VERSION_AUTOMATIC_OUTCOME: ${{ steps.bump-desktop-version-automatic.outcome }} + _CALCULATE_NEXT_DESKTOP_VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }} + _BUMP_WEB_VERSION_OVERRIDE_OUTCOME: ${{ steps.bump-web-version-override.outcome }} + _BUMP_WEB_VERSION_AUTOMATIC_OUTCOME: ${{ steps.bump-web-version-automatic.outcome }} + _CALCULATE_NEXT_WEB_VERSION: ${{ steps.calculate-next-web-version.outputs.version }} run: | - if [[ "${{ steps.bump-browser-version-override.outcome }}" = "success" ]]; then - echo "version_browser=$VERSION" >> $GITHUB_OUTPUT - elif [[ "${{ steps.bump-browser-version-automatic.outcome }}" = "success" ]]; then - echo "version_browser=${{ steps.calculate-next-browser-version.outputs.version }}" >> $GITHUB_OUTPUT + if [[ "$_BUMP_BROWSER_VERSION_OVERRIDE_OUTCOME" = "success" ]]; then + echo "version_browser=$VERSION" >> "$GITHUB_OUTPUT" + elif [[ "$_BUMP_BROWSER_VERSION_AUTOMATIC_OUTCOME" = "success" ]]; then + echo "version_browser=$_CALCULATE_NEXT_BROWSER_VERSION" >> "$GITHUB_OUTPUT" fi - if [[ "${{ steps.bump-cli-version-override.outcome }}" = "success" ]]; then - echo "version_cli=$VERSION" >> $GITHUB_OUTPUT - elif [[ "${{ steps.bump-cli-version-automatic.outcome }}" = "success" ]]; then - echo "version_cli=${{ steps.calculate-next-cli-version.outputs.version }}" >> $GITHUB_OUTPUT + if [[ "$_BUMP_CLI_VERSION_OVERRIDE_OUTCOME" = "success" ]]; then + echo "version_cli=$VERSION" >> "$GITHUB_OUTPUT" + elif [[ "$_BUMP_CLI_VERSION_AUTOMATIC_OUTCOME" = "success" ]]; then + echo "version_cli=$_CALCULATE_NEXT_CLI_VERSION" >> "$GITHUB_OUTPUT" fi - if [[ "${{ steps.bump-desktop-version-override.outcome }}" = "success" ]]; then - echo "version_desktop=$VERSION" >> $GITHUB_OUTPUT - elif [[ "${{ steps.bump-desktop-version-automatic.outcome }}" = "success" ]]; then - echo "version_desktop=${{ steps.calculate-next-desktop-version.outputs.version }}" >> $GITHUB_OUTPUT + if [[ "$_BUMP_DESKTOP_VERSION_OVERRIDE_OUTCOME" = "success" ]]; then + echo "version_desktop=$VERSION" >> "$GITHUB_OUTPUT" + elif [[ "$_BUMP_DESKTOP_VERSION_AUTOMATIC_OUTCOME" = "success" ]]; then + echo "version_desktop=$_CALCULATE_NEXT_DESKTOP_VERSION" >> "$GITHUB_OUTPUT" fi - if [[ "${{ steps.bump-web-version-override.outcome }}" = "success" ]]; then - echo "version_web=$VERSION" >> $GITHUB_OUTPUT - elif [[ "${{ steps.bump-web-version-automatic.outcome }}" = "success" ]]; then - echo "version_web=${{ steps.calculate-next-web-version.outputs.version }}" >> $GITHUB_OUTPUT + if [[ "$_BUMP_WEB_VERSION_OVERRIDE_OUTCOME" = "success" ]]; then + echo "version_web=$VERSION" >> "$GITHUB_OUTPUT" + elif [[ "$_BUMP_WEB_VERSION_AUTOMATIC_OUTCOME" = "success" ]]; then + echo "version_web=$_CALCULATE_NEXT_WEB_VERSION" >> "$GITHUB_OUTPUT" fi - name: Check if version changed id: version-changed run: | if [ -n "$(git status --porcelain)" ]; then - echo "changes_to_commit=TRUE" >> $GITHUB_OUTPUT + echo "changes_to_commit=TRUE" >> "$GITHUB_OUTPUT" else - echo "changes_to_commit=FALSE" >> $GITHUB_OUTPUT + echo "changes_to_commit=FALSE" >> "$GITHUB_OUTPUT" echo "No changes to commit!"; fi @@ -464,13 +473,14 @@ jobs: with: ref: ${{ inputs.target_ref }} token: ${{ steps.app-token.outputs.token }} + persist-credentials: true - name: Check if ${{ needs.setup.outputs.branch }} branch exists env: BRANCH_NAME: ${{ needs.setup.outputs.branch }} run: | - if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then - echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY + if [[ $(git ls-remote --heads origin "$BRANCH_NAME") ]]; then + echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> "$GITHUB_STEP_SUMMARY" exit 1 fi @@ -478,5 +488,5 @@ jobs: env: BRANCH_NAME: ${{ needs.setup.outputs.branch }} run: | - git switch --quiet --create $BRANCH_NAME - git push --quiet --set-upstream origin $BRANCH_NAME + git switch --quiet --create "$BRANCH_NAME" + git push --quiet --set-upstream origin "$BRANCH_NAME" diff --git a/.github/workflows/retrieve-current-desktop-rollout.yml b/.github/workflows/retrieve-current-desktop-rollout.yml index c45453ed9d0..30aef41e649 100644 --- a/.github/workflows/retrieve-current-desktop-rollout.yml +++ b/.github/workflows/retrieve-current-desktop-rollout.yml @@ -39,10 +39,10 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.aws-electron-access-key }} AWS_DEFAULT_REGION: 'us-west-2' AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.aws-electron-bucket-name }} - run: aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest.yml . --quiet + run: aws s3 cp "$AWS_S3_BUCKET_NAME/desktop/latest.yml" . --quiet - name: Get current rollout percentage run: | CURRENT_PCT=$(sed -r -n "s/stagingPercentage:\s([0-9]+)/\1/p" latest.yml) CURRENT_VERSION=$(sed -r -n "s/version:\s(.*)/\1/p" latest.yml) - echo "Desktop ${CURRENT_VERSION} rollout percentage is ${CURRENT_PCT}%" >> $GITHUB_STEP_SUMMARY + echo "Desktop ${CURRENT_VERSION} rollout percentage is ${CURRENT_PCT}%" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/staged-rollout-desktop.yml b/.github/workflows/staged-rollout-desktop.yml index 4adf81100bd..3d4f0376b39 100644 --- a/.github/workflows/staged-rollout-desktop.yml +++ b/.github/workflows/staged-rollout-desktop.yml @@ -47,11 +47,11 @@ jobs: AWS_DEFAULT_REGION: 'us-west-2' AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.aws-electron-bucket-name }} run: | - aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest.yml . \ + aws s3 cp "$AWS_S3_BUCKET_NAME/desktop/latest.yml" . \ --quiet - aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest-linux.yml . \ + aws s3 cp "$AWS_S3_BUCKET_NAME/desktop/latest-linux.yml" . \ --quiet - aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest-mac.yml . \ + aws s3 cp "$AWS_S3_BUCKET_NAME/desktop/latest-mac.yml" . \ --quiet - name: Check new rollout percentage @@ -86,11 +86,11 @@ jobs: AWS_DEFAULT_REGION: 'us-west-2' AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.aws-electron-bucket-name }} run: | - aws s3 cp latest.yml $AWS_S3_BUCKET_NAME/desktop/ \ + aws s3 cp latest.yml "$AWS_S3_BUCKET_NAME/desktop/" \ --acl "public-read" - aws s3 cp latest-linux.yml $AWS_S3_BUCKET_NAME/desktop/ \ + aws s3 cp latest-linux.yml "$AWS_S3_BUCKET_NAME/desktop/" \ --acl "public-read" - aws s3 cp latest-mac.yml $AWS_S3_BUCKET_NAME/desktop/ \ + aws s3 cp latest-mac.yml "$AWS_S3_BUCKET_NAME/desktop/" \ --acl "public-read" diff --git a/.github/workflows/test-browser-interactions.yml b/.github/workflows/test-browser-interactions.yml index 3af1a1a8e9d..a05f506d63f 100644 --- a/.github/workflows/test-browser-interactions.yml +++ b/.github/workflows/test-browser-interactions.yml @@ -21,6 +21,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 + persist-credentials: false - name: Check for job requirements if: ${{ !github.event.workflow_run.pull_requests || !github.event.workflow_run.head_branch }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 680bfb87cfe..cf62df3180f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,13 +25,15 @@ jobs: steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Get Node Version id: retrieve-node-version run: | NODE_NVMRC=$(cat .nvmrc) NODE_VERSION=${NODE_NVMRC/v/''} - echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT + echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT" - name: Set up Node uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 @@ -102,6 +104,8 @@ jobs: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Build working-directory: ./apps/desktop/desktop_native @@ -134,6 +138,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Install rust uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # stable @@ -168,6 +174,8 @@ jobs: steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Download jest coverage uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 diff --git a/.github/workflows/version-auto-bump.yml b/.github/workflows/version-auto-bump.yml index 3cb5646886a..0f7f2c9f46d 100644 --- a/.github/workflows/version-auto-bump.yml +++ b/.github/workflows/version-auto-bump.yml @@ -42,6 +42,7 @@ jobs: with: ref: main token: ${{ steps.app-token.outputs.token }} + persist-credentials: true - name: Configure Git run: | @@ -52,7 +53,7 @@ jobs: id: current-desktop-version run: | CURRENT_VERSION=$(cat package.json | jq -r '.version') - echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT" working-directory: apps/desktop - name: Calculate next Desktop release version @@ -65,12 +66,12 @@ jobs: id: bump-desktop-version-automatic env: VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }} - run: npm version --workspace=@bitwarden/desktop $VERSION + run: npm version --workspace=@bitwarden/desktop "$VERSION" - name: Bump Desktop Version - App - Automatic Calculation env: VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }} - run: npm version $VERSION + run: npm version "$VERSION" working-directory: "apps/desktop/src" - name: Commit files From 5b2682ff2156bd0ad1a52ad8f9ea36468ee37974 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:15:29 -0500 Subject: [PATCH 11/16] [PM-26356] Remove client-side pre-consolidated billing code (#16707) * Remove legacy provider files * Removing index files to make file re-org easier * Move manage-clients.component and associated API invocation to AC * Move add-existing-organization-dialog.component to AC * Move manage-client-name-dialog.component and associated API call to AC * Move misc clients files to AC * Move create-client-dialog.component and associated API call to AC * Move manage-client-subscription-dialog.component to AC * Update provider-layout.component * Cleanup * Fix linting --- .../members/members.component.ts | 6 +- .../unified-upgrade-dialog.component.spec.ts | 8 +- .../unified-upgrade-dialog.component.ts | 12 +- .../upgrade-account.component.ts | 8 +- .../upgrade-payment.component.ts | 6 +- .../vault-items/vault-items.component.ts | 2 +- ...xisting-organization-dialog.component.html | 0 ...-existing-organization-dialog.component.ts | 4 +- .../clients/add-organization.component.html | 32 ---- .../clients/add-organization.component.ts | 101 ---------- .../providers/clients/clients.component.html | 78 -------- .../providers/clients/clients.component.ts | 179 ------------------ .../create-client-dialog.component.html | 0 .../clients/create-client-dialog.component.ts | 2 +- .../create-organization.component.html | 3 - .../clients/create-organization.component.ts | 27 --- .../manage-client-name-dialog.component.html | 0 .../manage-client-name-dialog.component.ts | 10 +- ...-client-subscription-dialog.component.html | 0 ...ge-client-subscription-dialog.component.ts | 8 +- .../clients/manage-clients.component.html | 0 .../clients/manage-clients.component.ts | 61 +++--- .../providers/clients/no-clients.component.ts | 0 .../providers/clients/replace.pipe.ts | 0 .../providers/providers-layout.component.html | 2 +- .../providers/providers-layout.component.ts | 13 +- .../providers/providers-routing.module.ts | 25 +-- .../providers/providers.module.ts | 24 +-- .../services/web-provider.service.spec.ts | 10 +- .../services/web-provider.service.ts | 35 +--- .../app/billing/providers/clients/index.ts | 6 - .../guards/has-consolidated-billing.guard.ts | 22 --- .../src/app/billing/providers/index.ts | 7 - .../provider-api.service.abstraction.ts | 18 ++ .../create-provider-organization.request.ts} | 5 +- .../update-provider-organization.request.ts} | 2 +- .../services/provider/provider-api.service.ts | 44 +++++ .../billing-api.service.abstraction.ts | 18 -- .../billing/services/billing-api.service.ts | 43 ----- .../pricing-card/pricing-card.component.ts | 14 +- .../item-details/item-details-v2.component.ts | 20 +- 41 files changed, 169 insertions(+), 686 deletions(-) rename bitwarden_license/bit-web/src/app/{billing => admin-console}/providers/clients/add-existing-organization-dialog.component.html (100%) rename bitwarden_license/bit-web/src/app/{billing => admin-console}/providers/clients/add-existing-organization-dialog.component.ts (94%) delete mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-organization.component.html delete mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-organization.component.ts delete mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.html delete mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts rename bitwarden_license/bit-web/src/app/{billing => admin-console}/providers/clients/create-client-dialog.component.html (100%) rename bitwarden_license/bit-web/src/app/{billing => admin-console}/providers/clients/create-client-dialog.component.ts (98%) delete mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-organization.component.html delete mode 100644 bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-organization.component.ts rename bitwarden_license/bit-web/src/app/{billing => admin-console}/providers/clients/manage-client-name-dialog.component.html (100%) rename bitwarden_license/bit-web/src/app/{billing => admin-console}/providers/clients/manage-client-name-dialog.component.ts (82%) rename bitwarden_license/bit-web/src/app/{billing => admin-console}/providers/clients/manage-client-subscription-dialog.component.html (100%) rename bitwarden_license/bit-web/src/app/{billing => admin-console}/providers/clients/manage-client-subscription-dialog.component.ts (93%) rename bitwarden_license/bit-web/src/app/{billing => admin-console}/providers/clients/manage-clients.component.html (100%) rename bitwarden_license/bit-web/src/app/{billing => admin-console}/providers/clients/manage-clients.component.ts (86%) rename bitwarden_license/bit-web/src/app/{billing => admin-console}/providers/clients/no-clients.component.ts (100%) rename bitwarden_license/bit-web/src/app/{billing => admin-console}/providers/clients/replace.pipe.ts (100%) delete mode 100644 bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts delete mode 100644 bitwarden_license/bit-web/src/app/billing/providers/guards/has-consolidated-billing.guard.ts delete mode 100644 bitwarden_license/bit-web/src/app/billing/providers/index.ts rename libs/common/src/{billing/models/request/create-client-organization.request.ts => admin-console/models/request/create-provider-organization.request.ts} (66%) rename libs/common/src/{billing/models/request/update-client-organization.request.ts => admin-console/models/request/update-provider-organization.request.ts} (73%) diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index dd1c0edf08b..3841f6d5b4b 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -105,14 +105,14 @@ export class MembersComponent extends BaseMembersComponent memberTab = MemberDialogTab; protected dataSource = new MembersTableDataSource(); - organization: Signal; + readonly organization: Signal; status: OrganizationUserStatusType | undefined; orgResetPasswordPolicyEnabled = false; - protected canUseSecretsManager: Signal = computed( + protected readonly canUseSecretsManager: Signal = computed( () => this.organization()?.useSecretsManager ?? false, ); - protected showUserManagementControls: Signal = computed( + protected readonly showUserManagementControls: Signal = computed( () => this.organization()?.canManageUsers ?? false, ); private refreshBillingMetadata$: BehaviorSubject = new BehaviorSubject(null); diff --git a/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.spec.ts b/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.spec.ts index 505a4b9b7e4..1d707cec75f 100644 --- a/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.spec.ts +++ b/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.spec.ts @@ -32,8 +32,8 @@ import { standalone: true, }) class MockUpgradeAccountComponent { - dialogTitleMessageOverride = input(null); - hideContinueWithoutUpgradingButton = input(false); + readonly dialogTitleMessageOverride = input(null); + readonly hideContinueWithoutUpgradingButton = input(false); planSelected = output(); closeClicked = output(); } @@ -44,8 +44,8 @@ class MockUpgradeAccountComponent { standalone: true, }) class MockUpgradePaymentComponent { - selectedPlanId = input(null); - account = input(null); + readonly selectedPlanId = input(null); + readonly account = input(null); goBack = output(); complete = output(); } diff --git a/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.ts b/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.ts index 5a84856225d..0d9c8902d6c 100644 --- a/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.ts +++ b/apps/web/src/app/billing/individual/upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component.ts @@ -77,11 +77,13 @@ export type UnifiedUpgradeDialogParams = { }) export class UnifiedUpgradeDialogComponent implements OnInit { // Use signals for dialog state because inputs depend on parent component - protected step = signal(UnifiedUpgradeDialogStep.PlanSelection); - protected selectedPlan = signal(null); - protected account = signal(null); - protected planSelectionStepTitleOverride = signal(null); - protected hideContinueWithoutUpgradingButton = signal(false); + protected readonly step = signal( + UnifiedUpgradeDialogStep.PlanSelection, + ); + protected readonly selectedPlan = signal(null); + protected readonly account = signal(null); + protected readonly planSelectionStepTitleOverride = signal(null); + protected readonly hideContinueWithoutUpgradingButton = signal(false); protected readonly PaymentStep = UnifiedUpgradeDialogStep.Payment; protected readonly PlanSelectionStep = UnifiedUpgradeDialogStep.PlanSelection; diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.ts index a9d9b959282..c9b8f22d046 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.ts +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-account/upgrade-account.component.ts @@ -52,11 +52,11 @@ type CardDetails = { templateUrl: "./upgrade-account.component.html", }) export class UpgradeAccountComponent implements OnInit { - dialogTitleMessageOverride = input(null); - hideContinueWithoutUpgradingButton = input(false); + readonly dialogTitleMessageOverride = input(null); + readonly hideContinueWithoutUpgradingButton = input(false); planSelected = output(); closeClicked = output(); - protected loading = signal(true); + protected readonly loading = signal(true); protected premiumCardDetails!: CardDetails; protected familiesCardDetails!: CardDetails; @@ -64,7 +64,7 @@ export class UpgradeAccountComponent implements OnInit { protected premiumPlanType = PersonalSubscriptionPricingTierIds.Premium; protected closeStatus = UpgradeAccountStatus.Closed; - protected dialogTitle = computed(() => { + protected readonly dialogTitle = computed(() => { return this.dialogTitleMessageOverride() || "individualUpgradeWelcomeMessage"; }); diff --git a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts index bd88c22f98d..0b785d44e95 100644 --- a/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts +++ b/apps/web/src/app/billing/individual/upgrade/upgrade-payment/upgrade-payment.component.ts @@ -75,8 +75,8 @@ export type UpgradePaymentParams = { templateUrl: "./upgrade-payment.component.html", }) export class UpgradePaymentComponent implements OnInit, AfterViewInit { - protected selectedPlanId = input.required(); - protected account = input.required(); + protected readonly selectedPlanId = input.required(); + protected readonly account = input.required(); protected goBack = output(); protected complete = output(); protected selectedPlan: PlanDetails | null = null; @@ -90,7 +90,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit { billingAddress: EnterBillingAddressComponent.getFormGroup(), }); - protected loading = signal(true); + protected readonly loading = signal(true); private pricingTiers$!: Observable; // Cart Summary data diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 67a5069034f..81ad29db9dd 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -67,7 +67,7 @@ export class VaultItemsComponent { @Input() userCanArchive: boolean; @Input() enforceOrgDataOwnershipPolicy: boolean; - private restrictedPolicies = toSignal(this.restrictedItemTypesService.restricted$); + private readonly restrictedPolicies = toSignal(this.restrictedItemTypesService.restricted$); private _ciphers?: C[] = []; @Input() get ciphers(): C[] { diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-existing-organization-dialog.component.html similarity index 100% rename from bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.html rename to bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-existing-organization-dialog.component.html diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-existing-organization-dialog.component.ts similarity index 94% rename from bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.ts rename to bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-existing-organization-dialog.component.ts index f0eda893d67..a99d86b6e96 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/add-existing-organization-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-existing-organization-dialog.component.ts @@ -12,7 +12,7 @@ import { ToastService, } from "@bitwarden/components"; -import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; +import { WebProviderService } from "../services/web-provider.service"; export type AddExistingOrganizationDialogParams = { provider: Provider; @@ -55,7 +55,7 @@ export class AddExistingOrganizationDialogComponent implements OnInit { addExistingOrganization = async (): Promise => { if (this.selectedOrganization) { - await this.webProviderService.addOrganizationToProviderVNext( + await this.webProviderService.addOrganizationToProvider( this.dialogParams.provider.id, this.selectedOrganization.id, ); diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-organization.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-organization.component.html deleted file mode 100644 index 82e412c2ef6..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-organization.component.html +++ /dev/null @@ -1,32 +0,0 @@ - - {{ "addExistingOrganization" | i18n }} - - - - - - - - - {{ o.name }} - - - - - - - - - - - - diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-organization.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-organization.component.ts deleted file mode 100644 index 4f4b85e544b..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/add-organization.component.ts +++ /dev/null @@ -1,101 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, Inject, OnInit } from "@angular/core"; -import { Observable, switchMap } from "rxjs"; - -import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { DIALOG_DATA, DialogRef, DialogService, ToastService } from "@bitwarden/components"; - -import { WebProviderService } from "../services/web-provider.service"; - -interface AddOrganizationDialogData { - providerId: string; - organizations: Organization[]; -} - -@Component({ - templateUrl: "add-organization.component.html", - standalone: false, -}) -export class AddOrganizationComponent implements OnInit { - protected provider$: Observable; - protected loading = true; - - constructor( - private dialogRef: DialogRef, - @Inject(DIALOG_DATA) protected data: AddOrganizationDialogData, - private providerService: ProviderService, - private webProviderService: WebProviderService, - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private validationService: ValidationService, - private dialogService: DialogService, - private toastService: ToastService, - private accountService: AccountService, - ) {} - - async ngOnInit() { - await this.load(); - } - - async load() { - if (this.data.providerId == null) { - return; - } - - this.provider$ = this.accountService.activeAccount$.pipe( - getUserId, - switchMap((userId) => this.providerService.get$(this.data.providerId, userId)), - ); - - this.loading = false; - } - - add(organization: Organization, provider: Provider) { - return async () => { - const confirmed = await this.dialogService.openSimpleDialog({ - title: organization.name, - content: { - key: "addOrganizationConfirmation", - placeholders: [organization.name, provider.name], - }, - type: "warning", - }); - - if (!confirmed) { - return false; - } - - try { - await this.webProviderService.addOrganizationToProvider( - this.data.providerId, - organization.id, - ); - } catch (e) { - this.validationService.showError(e); - return; - } - - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("organizationJoinedProvider"), - }); - - this.dialogRef.close(true); - }; - } - - static open(dialogService: DialogService, data: AddOrganizationDialogData) { - return dialogService.open(AddOrganizationComponent, { - data, - }); - } -} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.html deleted file mode 100644 index 8f740384305..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.html +++ /dev/null @@ -1,78 +0,0 @@ -@let isAdminOrServiceUser = isAdminOrServiceUser$ | async; - - - - - {{ "newClient" | i18n }} - - - - - - - {{ "loading" | i18n }} - - - -

{{ "noClientsInList" | i18n }}

- - - - {{ "name" | i18n }} - {{ "numberOfUsers" | i18n }} - {{ "billingPlan" | i18n }} - - - - - - - {{ row.organizationName }} - - - {{ row.userCount }} - / {{ row.seats }} - - - {{ row.plan }} -
- - - - -
- -
-
-
-
diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts deleted file mode 100644 index 6d0b52b05d6..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { FormControl } from "@angular/forms"; -import { ActivatedRoute, Router, RouterModule } from "@angular/router"; -import { combineLatest, firstValueFrom, from, map, Observable, switchMap } from "rxjs"; -import { debounceTime, first } from "rxjs/operators"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { PlanType } from "@bitwarden/common/billing/enums"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { - AvatarModule, - DialogService, - TableDataSource, - TableModule, - ToastService, -} from "@bitwarden/components"; -import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; -import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; - -import { WebProviderService } from "../services/web-provider.service"; - -import { AddOrganizationComponent } from "./add-organization.component"; - -const DisallowedPlanTypes = [ - PlanType.Free, - PlanType.FamiliesAnnually2019, - PlanType.FamiliesAnnually, - PlanType.TeamsStarter2023, - PlanType.TeamsStarter, -]; - -@Component({ - templateUrl: "clients.component.html", - imports: [ - SharedOrganizationModule, - HeaderModule, - CommonModule, - JslibModule, - AvatarModule, - RouterModule, - TableModule, - ], -}) -export class ClientsComponent { - addableOrganizations: Organization[] = []; - loading = true; - showAddExisting = false; - dataSource: TableDataSource = - new TableDataSource(); - protected searchControl = new FormControl("", { nonNullable: true }); - - protected providerId$: Observable = - this.activatedRoute.parent?.params.pipe(map((params) => params.providerId as string)) ?? - new Observable(); - - protected provider$ = combineLatest([ - this.providerId$, - this.accountService.activeAccount$.pipe(getUserId), - ]).pipe(switchMap(([providerId, userId]) => this.providerService.get$(providerId, userId))); - - protected isAdminOrServiceUser$ = this.provider$.pipe( - map( - (provider) => - provider?.type === ProviderUserType.ProviderAdmin || - provider?.type === ProviderUserType.ServiceUser, - ), - ); - - constructor( - private router: Router, - private providerService: ProviderService, - private apiService: ApiService, - private organizationService: OrganizationService, - private organizationApiService: OrganizationApiServiceAbstraction, - private accountService: AccountService, - private activatedRoute: ActivatedRoute, - private dialogService: DialogService, - private i18nService: I18nService, - private toastService: ToastService, - private validationService: ValidationService, - private webProviderService: WebProviderService, - ) { - this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { - this.searchControl.setValue(queryParams.search); - }); - - this.provider$ - .pipe( - map((provider) => { - if (provider?.providerStatus === ProviderStatusType.Billable) { - return from( - this.router.navigate(["../manage-client-organizations"], { - relativeTo: this.activatedRoute, - }), - ); - } - return from(this.load()); - }), - takeUntilDestroyed(), - ) - .subscribe(); - - this.searchControl.valueChanges - .pipe(debounceTime(200), takeUntilDestroyed()) - .subscribe((searchText) => { - this.dataSource.filter = (data) => - data.organizationName.toLowerCase().indexOf(searchText.toLowerCase()) > -1; - }); - } - - async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: organization.organizationName, - content: { key: "detachOrganizationConfirmation" }, - type: "warning", - }); - - if (!confirmed) { - return; - } - - try { - const providerId = await firstValueFrom(this.providerId$); - await this.webProviderService.detachOrganization(providerId, organization.id); - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("detachedOrganization", organization.organizationName), - }); - await this.load(); - } catch (e) { - this.validationService.showError(e); - } - } - - async load() { - const providerId = await firstValueFrom(this.providerId$); - const response = await this.apiService.getProviderClients(providerId); - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - const clients = response.data != null && response.data.length > 0 ? response.data : []; - this.dataSource.data = clients; - const candidateOrgs = ( - await firstValueFrom(this.organizationService.organizations$(userId)) - ).filter((o) => o.isOwner && o.providerId == null); - const allowedOrgsIds = await Promise.all( - candidateOrgs.map((o) => this.organizationApiService.get(o.id)), - ).then((orgs) => - orgs.filter((o) => !DisallowedPlanTypes.includes(o.planType)).map((o) => o.id), - ); - this.addableOrganizations = candidateOrgs.filter((o) => allowedOrgsIds.includes(o.id)); - - this.showAddExisting = this.addableOrganizations.length !== 0; - this.loading = false; - } - - async addExistingOrganization() { - const providerId = await firstValueFrom(this.providerId$); - const dialogRef = AddOrganizationComponent.open(this.dialogService, { - providerId: providerId, - organizations: this.addableOrganizations, - }); - - if (await firstValueFrom(dialogRef.closed)) { - await this.load(); - } - } -} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-client-dialog.component.html similarity index 100% rename from bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html rename to bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-client-dialog.component.html diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-client-dialog.component.ts similarity index 98% rename from bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts rename to bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-client-dialog.component.ts index 2d6a0a75509..73e642dfa06 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-client-dialog.component.ts @@ -17,7 +17,7 @@ import { ToastService, } from "@bitwarden/components"; -import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; +import { WebProviderService } from "../services/web-provider.service"; type CreateClientDialogParams = { providerId: string; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-organization.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-organization.component.html deleted file mode 100644 index 1aa720b0485..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-organization.component.html +++ /dev/null @@ -1,3 +0,0 @@ - -

{{ "newClientOrganizationDesc" | i18n }}

- diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-organization.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-organization.component.ts deleted file mode 100644 index 9f3582f84bb..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/create-organization.component.ts +++ /dev/null @@ -1,27 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnInit, ViewChild } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; - -import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing"; - -@Component({ - selector: "app-create-organization", - templateUrl: "create-organization.component.html", - standalone: false, -}) -export class CreateOrganizationComponent implements OnInit { - @ViewChild(OrganizationPlansComponent, { static: true }) - orgPlansComponent: OrganizationPlansComponent; - - providerId: string; - - constructor(private route: ActivatedRoute) {} - - ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.parent.params.subscribe(async (params) => { - this.providerId = params.providerId; - }); - } -} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-name-dialog.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-client-name-dialog.component.html similarity index 100% rename from bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-name-dialog.component.html rename to bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-client-name-dialog.component.html diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-name-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-client-name-dialog.component.ts similarity index 82% rename from bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-name-dialog.component.ts rename to bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-client-name-dialog.component.ts index 06d1f80c101..045c9d8e8df 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-name-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-client-name-dialog.component.ts @@ -3,8 +3,8 @@ import { Component, Inject } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; -import { UpdateClientOrganizationRequest } from "@bitwarden/common/billing/models/request/update-client-organization.request"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; +import { UpdateProviderOrganizationRequest } from "@bitwarden/common/admin-console/models/request/update-provider-organization.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DIALOG_DATA, @@ -51,7 +51,7 @@ export class ManageClientNameDialogComponent { constructor( @Inject(DIALOG_DATA) protected dialogParams: ManageClientNameDialogParams, - private billingApiService: BillingApiServiceAbstraction, + private providerApiService: ProviderApiServiceAbstraction, private dialogRef: DialogRef, private formBuilder: FormBuilder, private i18nService: I18nService, @@ -65,11 +65,11 @@ export class ManageClientNameDialogComponent { return; } - const request = new UpdateClientOrganizationRequest(); + const request = new UpdateProviderOrganizationRequest(); request.assignedSeats = this.dialogParams.organization.seats; request.name = this.formGroup.value.name; - await this.billingApiService.updateProviderClientOrganization( + await this.providerApiService.updateProviderOrganization( this.dialogParams.providerId, this.dialogParams.organization.id, request, diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-client-subscription-dialog.component.html similarity index 100% rename from bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.html rename to bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-client-subscription-dialog.component.html diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-client-subscription-dialog.component.ts similarity index 93% rename from bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts rename to bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-client-subscription-dialog.component.ts index 71e549b563b..4c80402d3f7 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-subscription-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-client-subscription-dialog.component.ts @@ -3,11 +3,12 @@ import { Component, Inject, OnInit } from "@angular/core"; import { FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators } from "@angular/forms"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { UpdateProviderOrganizationRequest } from "@bitwarden/common/admin-console/models/request/update-provider-organization.request"; import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; -import { UpdateClientOrganizationRequest } from "@bitwarden/common/billing/models/request/update-client-organization.request"; import { ProviderPlanResponse } from "@bitwarden/common/billing/models/response/provider-subscription-response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components"; @@ -56,6 +57,7 @@ export class ManageClientSubscriptionDialogComponent implements OnInit { constructor( private billingApiService: BillingApiServiceAbstraction, + private providerApiService: ProviderApiServiceAbstraction, @Inject(DIALOG_DATA) protected dialogParams: ManageClientSubscriptionDialogParams, private dialogRef: DialogRef, private i18nService: I18nService, @@ -99,11 +101,11 @@ export class ManageClientSubscriptionDialogComponent implements OnInit { } try { - const request = new UpdateClientOrganizationRequest(); + const request = new UpdateProviderOrganizationRequest(); request.assignedSeats = this.formGroup.value.assignedSeats; request.name = this.dialogParams.organization.organizationName; - await this.billingApiService.updateProviderClientOrganization( + await this.providerApiService.updateProviderOrganization( this.dialogParams.provider.id, this.dialogParams.organization.id, request, diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-clients.component.html similarity index 100% rename from bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html rename to bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-clients.component.html diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-clients.component.ts similarity index 86% rename from bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts rename to bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-clients.component.ts index 9933c316869..a3601d2c812 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/manage-clients.component.ts @@ -1,25 +1,21 @@ -import { Component } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { Component, OnDestroy, OnInit } from "@angular/core"; import { FormControl } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; +import { ActivatedRoute } from "@angular/router"; import { firstValueFrom, - from, lastValueFrom, map, combineLatest, switchMap, Observable, + Subject, + takeUntil, } from "rxjs"; import { debounceTime, first } from "rxjs/operators"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { - ProviderStatusType, - ProviderType, - ProviderUserType, -} from "@bitwarden/common/admin-console/enums"; -import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { ProviderType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; @@ -40,7 +36,7 @@ import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service"; import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; -import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; +import { WebProviderService } from "../services/web-provider.service"; import { AddExistingOrganizationDialogComponent, @@ -72,7 +68,7 @@ import { ReplacePipe } from "./replace.pipe"; ReplacePipe, ], }) -export class ManageClientsComponent { +export class ManageClientsComponent implements OnInit, OnDestroy { loading = true; dataSource: TableDataSource = new TableDataSource(); @@ -117,10 +113,11 @@ export class ManageClientsComponent { ), ); + private destroy$ = new Subject(); + constructor( private billingApiService: BillingApiServiceAbstraction, private providerService: ProviderService, - private router: Router, private activatedRoute: ActivatedRoute, private dialogService: DialogService, private i18nService: I18nService, @@ -130,35 +127,31 @@ export class ManageClientsComponent { private billingNotificationService: BillingNotificationService, private configService: ConfigService, private accountService: AccountService, - ) { - this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { - this.searchControl.setValue(queryParams.search); - }); + private providerApiService: ProviderApiServiceAbstraction, + ) {} - this.provider$ - .pipe( - map((provider: Provider | undefined) => { - if (provider?.providerStatus !== ProviderStatusType.Billable) { - return from( - this.router.navigate(["../clients"], { - relativeTo: this.activatedRoute, - }), - ); - } - return from(this.load()); - }), - takeUntilDestroyed(), - ) - .subscribe(); + async ngOnInit() { + this.activatedRoute.queryParams + .pipe(first(), takeUntil(this.destroy$)) + .subscribe((queryParams) => { + this.searchControl.setValue(queryParams.search); + }); + + await this.load(); this.searchControl.valueChanges - .pipe(debounceTime(200), takeUntilDestroyed()) + .pipe(debounceTime(200), takeUntil(this.destroy$)) .subscribe((searchText) => { this.dataSource.filter = (data) => data.organizationName.toLowerCase().indexOf(searchText.toLowerCase()) > -1; }); } + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + async load() { try { const providerId = await firstValueFrom(this.providerId$); @@ -170,7 +163,7 @@ export class ManageClientsComponent { this.newClientButtonLabel = this.i18nService.t("newBusinessUnit"); } this.dataSource.data = ( - await this.billingApiService.getProviderClientOrganizations(providerId) + await this.providerApiService.getProviderOrganizations(providerId) ).data; this.plans = (await this.billingApiService.getPlans()).data; this.loading = false; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/no-clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/no-clients.component.ts similarity index 100% rename from bitwarden_license/bit-web/src/app/billing/providers/clients/no-clients.component.ts rename to bitwarden_license/bit-web/src/app/admin-console/providers/clients/no-clients.component.ts diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/replace.pipe.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/replace.pipe.ts similarity index 100% rename from bitwarden_license/bit-web/src/app/billing/providers/clients/replace.pipe.ts rename to bitwarden_license/bit-web/src/app/admin-console/providers/clients/replace.pipe.ts diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html index 724e5891dc8..ab4aaa6bd69 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html @@ -9,7 +9,7 @@ ; - protected isBillable: Observable; protected canAccessBilling$: Observable; protected clientsTranslationKey$: Observable; @@ -83,15 +82,7 @@ export class ProvidersLayoutComponent implements OnInit, OnDestroy { ), ); - this.isBillable = this.provider$.pipe( - map((provider) => provider?.providerStatus === ProviderStatusType.Billable), - ); - - this.canAccessBilling$ = combineLatest([this.isBillable, this.provider$]).pipe( - map( - ([hasConsolidatedBilling, provider]) => hasConsolidatedBilling && provider.isProviderAdmin, - ), - ); + this.canAccessBilling$ = this.provider$.pipe(map((provider) => provider.isProviderAdmin)); this.clientsTranslationKey$ = this.provider$.pipe( map((provider) => diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts index 7a554275f08..cf0dbe05d07 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts @@ -7,17 +7,12 @@ import { AnonLayoutWrapperComponent } from "@bitwarden/components"; import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component"; import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component"; -import { - ManageClientsComponent, - ProviderSubscriptionComponent, - hasConsolidatedBilling, - ProviderBillingHistoryComponent, -} from "../../billing/providers"; +import { ProviderBillingHistoryComponent } from "../../billing/providers/billing-history/provider-billing-history.component"; import { ProviderPaymentDetailsComponent } from "../../billing/providers/payment-details/provider-payment-details.component"; import { SetupBusinessUnitComponent } from "../../billing/providers/setup/setup-business-unit.component"; +import { ProviderSubscriptionComponent } from "../../billing/providers/subscription/provider-subscription.component"; -import { ClientsComponent } from "./clients/clients.component"; -import { CreateOrganizationComponent } from "./clients/create-organization.component"; +import { ManageClientsComponent } from "./clients/manage-clients.component"; import { providerPermissionsGuard } from "./guards/provider-permissions.guard"; import { AcceptProviderComponent } from "./manage/accept-provider.component"; import { EventsComponent } from "./manage/events.component"; @@ -88,14 +83,7 @@ const routes: Routes = [ canActivate: [providerPermissionsGuard()], children: [ { path: "", pathMatch: "full", redirectTo: "clients" }, - { path: "clients/create", component: CreateOrganizationComponent }, - { path: "clients", component: ClientsComponent, data: { titleId: "clients" } }, - { - path: "manage-client-organizations", - canActivate: [hasConsolidatedBilling], - component: ManageClientsComponent, - data: { titleId: "clients" }, - }, + { path: "clients", component: ManageClientsComponent, data: { titleId: "clients" } }, { path: "manage", children: [ @@ -128,7 +116,7 @@ const routes: Routes = [ }, { path: "billing", - canActivate: [hasConsolidatedBilling], + canActivate: [providerPermissionsGuard()], children: [ { path: "", @@ -138,7 +126,6 @@ const routes: Routes = [ { path: "subscription", component: ProviderSubscriptionComponent, - canActivate: [providerPermissionsGuard()], data: { titleId: "subscription", }, @@ -146,7 +133,6 @@ const routes: Routes = [ { path: "payment-details", component: ProviderPaymentDetailsComponent, - canActivate: [providerPermissionsGuard()], data: { titleId: "paymentDetails", }, @@ -154,7 +140,6 @@ const routes: Routes = [ { path: "history", component: ProviderBillingHistoryComponent, - canActivate: [providerPermissionsGuard()], data: { titleId: "billingHistory", }, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index 24e8a757bdf..92b16699b7e 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -13,22 +13,18 @@ import { } from "@bitwarden/web-vault/app/billing/payment/components"; import { OssModule } from "@bitwarden/web-vault/app/oss.module"; -import { - CreateClientDialogComponent, - InvoicesComponent, - ManageClientNameDialogComponent, - ManageClientSubscriptionDialogComponent, - NoInvoicesComponent, - ProviderBillingHistoryComponent, - ProviderSubscriptionComponent, - ProviderSubscriptionStatusComponent, -} from "../../billing/providers"; -import { AddExistingOrganizationDialogComponent } from "../../billing/providers/clients/add-existing-organization-dialog.component"; +import { InvoicesComponent } from "../../billing/providers/billing-history/invoices.component"; +import { NoInvoicesComponent } from "../../billing/providers/billing-history/no-invoices.component"; +import { ProviderBillingHistoryComponent } from "../../billing/providers/billing-history/provider-billing-history.component"; import { SetupBusinessUnitComponent } from "../../billing/providers/setup/setup-business-unit.component"; +import { ProviderSubscriptionStatusComponent } from "../../billing/providers/subscription/provider-subscription-status.component"; +import { ProviderSubscriptionComponent } from "../../billing/providers/subscription/provider-subscription.component"; import { ProviderWarningsModule } from "../../billing/providers/warnings/provider-warnings.module"; -import { AddOrganizationComponent } from "./clients/add-organization.component"; -import { CreateOrganizationComponent } from "./clients/create-organization.component"; +import { AddExistingOrganizationDialogComponent } from "./clients/add-existing-organization-dialog.component"; +import { CreateClientDialogComponent } from "./clients/create-client-dialog.component"; +import { ManageClientNameDialogComponent } from "./clients/manage-client-name-dialog.component"; +import { ManageClientSubscriptionDialogComponent } from "./clients/manage-client-subscription-dialog.component"; import { AcceptProviderComponent } from "./manage/accept-provider.component"; import { AddEditMemberDialogComponent } from "./manage/dialogs/add-edit-member-dialog.component"; import { BulkConfirmDialogComponent } from "./manage/dialogs/bulk-confirm-dialog.component"; @@ -65,10 +61,8 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr declarations: [ AcceptProviderComponent, AccountComponent, - AddOrganizationComponent, BulkConfirmDialogComponent, BulkRemoveDialogComponent, - CreateOrganizationComponent, EventsComponent, MembersComponent, SetupComponent, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.spec.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.spec.ts index 2ddfbabf885..b2da18dd047 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.spec.ts @@ -3,8 +3,6 @@ import { MockProxy, mock } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { PlanType } from "@bitwarden/common/billing/enums"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; @@ -26,10 +24,8 @@ describe("WebProviderService", () => { let apiService: MockProxy; let i18nService: MockProxy; let encryptService: MockProxy; - let billingApiService: MockProxy; let stateProvider: MockProxy; let providerApiService: MockProxy; - let accountService: MockProxy; beforeEach(() => { keyService = mock(); @@ -37,10 +33,8 @@ describe("WebProviderService", () => { apiService = mock(); i18nService = mock(); encryptService = mock(); - billingApiService = mock(); stateProvider = mock(); providerApiService = mock(); - accountService = mock(); sut = new WebProviderService( keyService, @@ -48,10 +42,8 @@ describe("WebProviderService", () => { apiService, i18nService, encryptService, - billingApiService, stateProvider, providerApiService, - accountService, ); }); @@ -99,7 +91,7 @@ describe("WebProviderService", () => { expect(keyService.getProviderKey).toHaveBeenCalledWith(providerId); expect(encryptService.wrapSymmetricKey).toHaveBeenCalledWith(mockOrgKey, mockProviderKey); - expect(billingApiService.createProviderClientOrganization).toHaveBeenCalledWith( + expect(providerApiService.createProviderOrganization).toHaveBeenCalledWith( providerId, expect.objectContaining({ name, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts index 16c2ab5cd3e..78931f9c445 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/services/web-provider.service.ts @@ -6,13 +6,9 @@ import { switchMap } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; +import { CreateProviderOrganizationRequest } from "@bitwarden/common/admin-console/models/request/create-provider-organization.request"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; -import { ProviderAddOrganizationRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-add-organization.request"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { PlanType } from "@bitwarden/common/billing/enums"; -import { CreateClientOrganizationRequest } from "@bitwarden/common/billing/models/request/create-client-organization.request"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateProvider } from "@bitwarden/common/platform/state"; @@ -29,34 +25,11 @@ export class WebProviderService { private apiService: ApiService, private i18nService: I18nService, private encryptService: EncryptService, - private billingApiService: BillingApiServiceAbstraction, private stateProvider: StateProvider, private providerApiService: ProviderApiServiceAbstraction, - private accountService: AccountService, ) {} - async addOrganizationToProvider(providerId: string, organizationId: string) { - const orgKey = await firstValueFrom( - this.accountService.activeAccount$.pipe( - getUserId, - switchMap((userId) => this.keyService.orgKeys$(userId)), - map((orgKeys) => orgKeys[organizationId as OrganizationId] ?? null), - ), - ); - const providerKey = await this.keyService.getProviderKey(providerId); - - const encryptedOrgKey = await this.encryptService.wrapSymmetricKey(orgKey, providerKey); - - const request = new ProviderAddOrganizationRequest(); - request.organizationId = organizationId; - request.key = encryptedOrgKey.encryptedString; - - const response = await this.apiService.postProviderAddOrganization(providerId, request); - await this.syncService.fullSync(true); - return response; - } - - async addOrganizationToProviderVNext(providerId: string, organizationId: string): Promise { + async addOrganizationToProvider(providerId: string, organizationId: string): Promise { const orgKey = await firstValueFrom( this.stateProvider.activeUserId$.pipe( switchMap((userId) => this.keyService.orgKeys$(userId)), @@ -96,7 +69,7 @@ export class WebProviderService { providerKey, ); - const request = new CreateClientOrganizationRequest(); + const request = new CreateProviderOrganizationRequest(); request.name = name; request.ownerEmail = ownerEmail; request.planType = planType; @@ -106,7 +79,7 @@ export class WebProviderService { request.keyPair = new OrganizationKeysRequest(publicKey, encryptedPrivateKey.encryptedString); request.collectionName = encryptedCollectionName.encryptedString; - await this.billingApiService.createProviderClientOrganization(providerId, request); + await this.providerApiService.createProviderOrganization(providerId, request); await this.apiService.refreshIdentityToken(); diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts deleted file mode 100644 index 898d51e0baf..00000000000 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./create-client-dialog.component"; -export * from "./manage-clients.component"; -export * from "./manage-client-name-dialog.component"; -export * from "./manage-client-subscription-dialog.component"; -export * from "./no-clients.component"; -export * from "./replace.pipe"; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/guards/has-consolidated-billing.guard.ts b/bitwarden_license/bit-web/src/app/billing/providers/guards/has-consolidated-billing.guard.ts deleted file mode 100644 index de13cca3ad4..00000000000 --- a/bitwarden_license/bit-web/src/app/billing/providers/guards/has-consolidated-billing.guard.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { inject } from "@angular/core"; -import { ActivatedRouteSnapshot, CanActivateFn, createUrlTreeFromSnapshot } from "@angular/router"; -import { firstValueFrom } from "rxjs"; - -import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderStatusType } from "@bitwarden/common/admin-console/enums"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; - -export const hasConsolidatedBilling: CanActivateFn = async (route: ActivatedRouteSnapshot) => { - const providerService = inject(ProviderService); - const accountService = inject(AccountService); - - const userId = await firstValueFrom(getUserId(accountService.activeAccount$)); - const provider = await firstValueFrom(providerService.get$(route.params.providerId, userId)); - - if (!provider || provider.providerStatus !== ProviderStatusType.Billable) { - return createUrlTreeFromSnapshot(route, ["/providers", route.params.providerId]); - } - - return true; -}; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/index.ts b/bitwarden_license/bit-web/src/app/billing/providers/index.ts deleted file mode 100644 index 3cd83e68990..00000000000 --- a/bitwarden_license/bit-web/src/app/billing/providers/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from "./billing-history/invoices.component"; -export * from "./billing-history/no-invoices.component"; -export * from "./billing-history/provider-billing-history.component"; -export * from "./clients"; -export * from "./guards/has-consolidated-billing.guard"; -export * from "./subscription/provider-subscription.component"; -export * from "./subscription/provider-subscription-status.component"; diff --git a/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts index f998fdc8ab7..0fba60a5944 100644 --- a/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts @@ -1,8 +1,12 @@ import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response"; +import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { CreateProviderOrganizationRequest } from "../../models/request/create-provider-organization.request"; import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request"; import { ProviderUpdateRequest } from "../../models/request/provider/provider-update.request"; import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request"; +import { UpdateProviderOrganizationRequest } from "../../models/request/update-provider-organization.request"; import { ProviderResponse } from "../../models/response/provider/provider.response"; export abstract class ProviderApiServiceAbstraction { @@ -14,6 +18,9 @@ export abstract class ProviderApiServiceAbstraction { request: ProviderVerifyRecoverDeleteRequest, ): Promise; abstract deleteProvider(id: string): Promise; + abstract getProviderOrganizations( + providerId: string, + ): Promise>; abstract getProviderAddableOrganizations( providerId: string, ): Promise; @@ -24,4 +31,15 @@ export abstract class ProviderApiServiceAbstraction { organizationId: string; }, ): Promise; + + abstract updateProviderOrganization( + providerId: string, + organizationId: string, + request: UpdateProviderOrganizationRequest, + ): Promise; + + abstract createProviderOrganization( + providerId: string, + request: CreateProviderOrganizationRequest, + ): Promise; } diff --git a/libs/common/src/billing/models/request/create-client-organization.request.ts b/libs/common/src/admin-console/models/request/create-provider-organization.request.ts similarity index 66% rename from libs/common/src/billing/models/request/create-client-organization.request.ts rename to libs/common/src/admin-console/models/request/create-provider-organization.request.ts index c7078164993..ccb437b922d 100644 --- a/libs/common/src/billing/models/request/create-client-organization.request.ts +++ b/libs/common/src/admin-console/models/request/create-provider-organization.request.ts @@ -1,9 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { OrganizationKeysRequest } from "../../../admin-console/models/request/organization-keys.request"; import { PlanType } from "../../../billing/enums"; -export class CreateClientOrganizationRequest { +import { OrganizationKeysRequest } from "./organization-keys.request"; + +export class CreateProviderOrganizationRequest { name: string; ownerEmail: string; planType: PlanType; diff --git a/libs/common/src/billing/models/request/update-client-organization.request.ts b/libs/common/src/admin-console/models/request/update-provider-organization.request.ts similarity index 73% rename from libs/common/src/billing/models/request/update-client-organization.request.ts rename to libs/common/src/admin-console/models/request/update-provider-organization.request.ts index e21344b3d6b..f65ac42464f 100644 --- a/libs/common/src/billing/models/request/update-client-organization.request.ts +++ b/libs/common/src/admin-console/models/request/update-provider-organization.request.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -export class UpdateClientOrganizationRequest { +export class UpdateProviderOrganizationRequest { assignedSeats: number; name: string; } diff --git a/libs/common/src/admin-console/services/provider/provider-api.service.ts b/libs/common/src/admin-console/services/provider/provider-api.service.ts index dc82ec011f4..f0dbb424558 100644 --- a/libs/common/src/admin-console/services/provider/provider-api.service.ts +++ b/libs/common/src/admin-console/services/provider/provider-api.service.ts @@ -1,10 +1,14 @@ import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response"; +import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { ApiService } from "../../../abstractions/api.service"; import { ProviderApiServiceAbstraction } from "../../abstractions/provider/provider-api.service.abstraction"; +import { CreateProviderOrganizationRequest } from "../../models/request/create-provider-organization.request"; import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request"; import { ProviderUpdateRequest } from "../../models/request/provider/provider-update.request"; import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request"; +import { UpdateProviderOrganizationRequest } from "../../models/request/update-provider-organization.request"; import { ProviderResponse } from "../../models/response/provider/provider.response"; export class ProviderApiService implements ProviderApiServiceAbstraction { @@ -47,6 +51,19 @@ export class ProviderApiService implements ProviderApiServiceAbstraction { await this.apiService.send("DELETE", "/providers/" + id, null, true, false); } + async getProviderOrganizations( + providerId: string, + ): Promise> { + const response = await this.apiService.send( + "GET", + "/providers/" + providerId + "/organizations", + null, + true, + true, + ); + return new ListResponse(response, ProviderOrganizationOrganizationDetailsResponse); + } + async getProviderAddableOrganizations( providerId: string, ): Promise { @@ -76,4 +93,31 @@ export class ProviderApiService implements ProviderApiServiceAbstraction { false, ); } + + async updateProviderOrganization( + providerId: string, + organizationId: string, + request: UpdateProviderOrganizationRequest, + ): Promise { + return await this.apiService.send( + "PUT", + "/providers/" + providerId + "/clients/" + organizationId, + request, + true, + false, + ); + } + + createProviderOrganization( + providerId: string, + request: CreateProviderOrganizationRequest, + ): Promise { + return this.apiService.send( + "POST", + "/providers/" + providerId + "/clients", + request, + true, + false, + ); + } } diff --git a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts index 1dbb8053e97..d581fdaa95c 100644 --- a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts @@ -1,12 +1,9 @@ import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; -import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response"; import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response"; import { PlanResponse } from "../../billing/models/response/plan.response"; import { ListResponse } from "../../models/response/list.response"; import { OrganizationId } from "../../types/guid"; -import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; -import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request"; import { InvoicesResponse } from "../models/response/invoices.response"; import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; @@ -18,11 +15,6 @@ export abstract class BillingApiServiceAbstraction { abstract cancelPremiumUserSubscription(request: SubscriptionCancellationRequest): Promise; - abstract createProviderClientOrganization( - providerId: string, - request: CreateClientOrganizationRequest, - ): Promise; - abstract getOrganizationBillingMetadata( organizationId: OrganizationId, ): Promise; @@ -35,20 +27,10 @@ export abstract class BillingApiServiceAbstraction { abstract getProviderClientInvoiceReport(providerId: string, invoiceId: string): Promise; - abstract getProviderClientOrganizations( - providerId: string, - ): Promise>; - abstract getProviderInvoices(providerId: string): Promise; abstract getProviderSubscription(providerId: string): Promise; - abstract updateProviderClientOrganization( - providerId: string, - organizationId: string, - request: UpdateClientOrganizationRequest, - ): Promise; - abstract restartSubscription( organizationId: string, request: OrganizationCreateRequest, diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index c953d920055..165ebf5c3b4 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -3,13 +3,10 @@ import { ApiService } from "../../abstractions/api.service"; import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; -import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response"; import { ListResponse } from "../../models/response/list.response"; import { OrganizationId } from "../../types/guid"; import { BillingApiServiceAbstraction } from "../abstractions"; -import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; import { SubscriptionCancellationRequest } from "../models/request/subscription-cancellation.request"; -import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request"; import { InvoicesResponse } from "../models/response/invoices.response"; import { OrganizationBillingMetadataResponse } from "../models/response/organization-billing-metadata.response"; import { PlanResponse } from "../models/response/plan.response"; @@ -35,19 +32,6 @@ export class BillingApiService implements BillingApiServiceAbstraction { return this.apiService.send("POST", "/accounts/cancel", request, true, false); } - createProviderClientOrganization( - providerId: string, - request: CreateClientOrganizationRequest, - ): Promise { - return this.apiService.send( - "POST", - "/providers/" + providerId + "/clients", - request, - true, - false, - ); - } - async getOrganizationBillingMetadata( organizationId: OrganizationId, ): Promise { @@ -92,19 +76,6 @@ export class BillingApiService implements BillingApiServiceAbstraction { return response as string; } - async getProviderClientOrganizations( - providerId: string, - ): Promise> { - const response = await this.apiService.send( - "GET", - "/providers/" + providerId + "/organizations", - null, - true, - true, - ); - return new ListResponse(response, ProviderOrganizationOrganizationDetailsResponse); - } - async getProviderInvoices(providerId: string): Promise { const response = await this.apiService.send( "GET", @@ -127,20 +98,6 @@ export class BillingApiService implements BillingApiServiceAbstraction { return new ProviderSubscriptionResponse(response); } - async updateProviderClientOrganization( - providerId: string, - organizationId: string, - request: UpdateClientOrganizationRequest, - ): Promise { - return await this.apiService.send( - "PUT", - "/providers/" + providerId + "/clients/" + organizationId, - request, - true, - false, - ); - } - async restartSubscription( organizationId: string, request: OrganizationCreateRequest, diff --git a/libs/pricing/src/components/pricing-card/pricing-card.component.ts b/libs/pricing/src/components/pricing-card/pricing-card.component.ts index 022653aa9e4..a6d6cce2a64 100644 --- a/libs/pricing/src/components/pricing-card/pricing-card.component.ts +++ b/libs/pricing/src/components/pricing-card/pricing-card.component.ts @@ -21,16 +21,20 @@ import { imports: [BadgeModule, ButtonModule, IconModule, TypographyModule, CurrencyPipe], }) export class PricingCardComponent { - tagline = input.required(); - price = input<{ amount: number; cadence: "monthly" | "annually"; showPerUser?: boolean }>(); - button = input<{ + readonly tagline = input.required(); + readonly price = input<{ + amount: number; + cadence: "monthly" | "annually"; + showPerUser?: boolean; + }>(); + readonly button = input<{ type: ButtonType; text: string; disabled?: boolean; icon?: { type: string; position: "before" | "after" }; }>(); - features = input(); - activeBadge = input<{ text: string; variant?: BadgeVariant }>(); + readonly features = input(); + readonly activeBadge = input<{ text: string; variant?: BadgeVariant }>(); @Output() buttonClick = new EventEmitter(); diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts index c775d66baac..31ba5c82d9d 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts @@ -36,18 +36,18 @@ import { OrgIconDirective } from "../../components/org-icon.directive"; ], }) export class ItemDetailsV2Component { - hideOwner = input(false); - cipher = input.required(); - organization = input(); - folder = input(); - collections = input(); - showAllDetails = signal(false); + readonly hideOwner = input(false); + readonly cipher = input.required(); + readonly organization = input(); + readonly folder = input(); + readonly collections = input(); + readonly showAllDetails = signal(false); - showOwnership = computed(() => { + readonly showOwnership = computed(() => { return this.cipher().organizationId && this.organization() && !this.hideOwner(); }); - hasSmallScreen = toSignal( + readonly hasSmallScreen = toSignal( fromEvent(window, "resize").pipe( map(() => window.innerWidth), startWith(window.innerWidth), @@ -56,7 +56,7 @@ export class ItemDetailsV2Component { ); // Array to hold all details of item. Organization, Collections, and Folder - allItems = computed(() => { + readonly allItems = computed(() => { let items: any[] = []; if (this.showOwnership() && this.organization()) { items.push(this.organization()); @@ -70,7 +70,7 @@ export class ItemDetailsV2Component { return items; }); - showItems = computed(() => { + readonly showItems = computed(() => { if ( this.hasSmallScreen() && this.allItems().length > 2 && From 1c4eed55a87ee1411a07510149eada197c32f04b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:29:42 -0500 Subject: [PATCH 12/16] [deps]: Update actions/stale action to v10 (#16427) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Derek Nance --- .github/workflows/stale-bot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index 13acde2b0fc..246e0d48c5d 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -15,7 +15,7 @@ jobs: pull-requests: write steps: - name: 'Run stale action' - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 + uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 with: stale-issue-label: 'needs-reply' stale-pr-label: 'needs-changes' From 1c9f1dbd627e96ba3a52b2e61625e0c256436922 Mon Sep 17 00:00:00 2001 From: Matt Andreko Date: Tue, 21 Oct 2025 15:48:06 -0400 Subject: [PATCH 13/16] Fix extra period at end of variable causing dev deployment issue (#16970) --- .github/workflows/deploy-web.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 26a83e89773..4da674678b5 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -390,7 +390,7 @@ jobs: env: AZCOPY_AUTO_LOGIN_TYPE: AZCLI AZCOPY_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - _VAULT_NAME: ${{ steps.retrieve-secrets-azcopy.outputs.sa-bitwarden-web-vault-name }}. + _VAULT_NAME: ${{ steps.retrieve-secrets-azcopy.outputs.sa-bitwarden-web-vault-name }} run: | azcopy sync ./build "https://$_VAULT_NAME.blob.core.windows.net/$web/" \ --delete-destination="${{ inputs.force-delete-destination }}" --compare-hash="MD5" From d9e1bde5e09b9765073e71e5f6cd233ac12470ea Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:54:35 -0500 Subject: [PATCH 14/16] change equality checks to account for null and undefined (#16971) --- .../item-details-section.component.spec.ts | 35 +++++++++++++++++-- .../item-details-section.component.ts | 4 +-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts index 4da299ed039..67b5509c8ac 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts @@ -135,7 +135,7 @@ describe("ItemDetailsSectionComponent", () => { tick(); expect(cipherFormProvider.patchCipher).toHaveBeenCalled(); - const patchFn = cipherFormProvider.patchCipher.mock.lastCall[0]; + const patchFn = cipherFormProvider.patchCipher.mock.lastCall![0]; const updatedCipher = patchFn(new CipherView()); @@ -165,7 +165,7 @@ describe("ItemDetailsSectionComponent", () => { tick(); expect(cipherFormProvider.patchCipher).toHaveBeenCalled(); - const patchFn = cipherFormProvider.patchCipher.mock.lastCall[0]; + const patchFn = cipherFormProvider.patchCipher.mock.lastCall![0]; const updatedCipher = patchFn(new CipherView()); @@ -440,7 +440,7 @@ describe("ItemDetailsSectionComponent", () => { await fixture.whenStable(); expect(cipherFormProvider.patchCipher).toHaveBeenCalled(); - const patchFn = cipherFormProvider.patchCipher.mock.lastCall[0]; + const patchFn = cipherFormProvider.patchCipher.mock.lastCall![0]; const updatedCipher = patchFn(new CipherView()); @@ -691,6 +691,35 @@ describe("ItemDetailsSectionComponent", () => { expect(enableFormFields).toHaveBeenCalled(); }); }); + + describe("setFormState behavior with null/undefined", () => { + it("calls disableFormFields when organizationId value is null", async () => { + component.originalCipherView.organizationId = null as any; + getInitialCipherView.mockReturnValue(component.originalCipherView); + + await component.ngOnInit(); + + expect(disableFormFields).toHaveBeenCalled(); + }); + + it("calls disableFormFields when organizationId value is undefined", async () => { + component.originalCipherView.organizationId = undefined; + getInitialCipherView.mockReturnValue(component.originalCipherView); + + await component.ngOnInit(); + + expect(disableFormFields).toHaveBeenCalled(); + }); + + it("calls enableFormFields when organizationId has a string value", async () => { + component.originalCipherView.organizationId = "org-id" as any; + getInitialCipherView.mockReturnValue(component.originalCipherView); + + await component.ngOnInit(); + + expect(enableFormFields).toHaveBeenCalled(); + }); + }); }); describe("when an ownership change is not allowed", () => { diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts index 8877b4cbcea..ce0244bc759 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts @@ -125,7 +125,7 @@ export class ItemDetailsSectionComponent implements OnInit { this.itemDetailsForm.controls.organizationId.disabled || (!this.allowPersonalOwnership && this.config.originalCipher && - this.itemDetailsForm.controls.organizationId.value === null) + this.itemDetailsForm.controls.organizationId.value == null) ); } @@ -252,7 +252,7 @@ export class ItemDetailsSectionComponent implements OnInit { // When editing a cipher and the user cannot have personal ownership // and the cipher is is not within the organization - force the user to // move the cipher within the organization first before editing any other field - if (this.itemDetailsForm.controls.organizationId.value === null) { + if (this.itemDetailsForm.controls.organizationId.value == null) { this.cipherFormContainer.disableFormFields(); this.itemDetailsForm.controls.organizationId.enable(); this.favoriteButtonDisabled = true; From 228b42f20744c511353d50a57a41c47e204cc399 Mon Sep 17 00:00:00 2001 From: Vince Grassia <593223+vgrassia@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:48:19 -0400 Subject: [PATCH 15/16] Escape dollar sign in URL (#16975) --- .github/workflows/deploy-web.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 4da674678b5..5aa0918048b 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -392,7 +392,7 @@ jobs: AZCOPY_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} _VAULT_NAME: ${{ steps.retrieve-secrets-azcopy.outputs.sa-bitwarden-web-vault-name }} run: | - azcopy sync ./build "https://$_VAULT_NAME.blob.core.windows.net/$web/" \ + azcopy sync ./build "https://$_VAULT_NAME.blob.core.windows.net/\$web/" \ --delete-destination="${{ inputs.force-delete-destination }}" --compare-hash="MD5" - name: Log out from Azure From 453feb362f4a5c6550c3f38c7bef9926699ec11c Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Wed, 22 Oct 2025 06:21:25 -0400 Subject: [PATCH 16/16] build(nx): fix serve browser (#16972) --- apps/browser/project.json | 143 +++++--------------------------------- 1 file changed, 16 insertions(+), 127 deletions(-) diff --git a/apps/browser/project.json b/apps/browser/project.json index 9a8df56c170..e0297df773b 100644 --- a/apps/browser/project.json +++ b/apps/browser/project.json @@ -315,164 +315,53 @@ } }, "serve": { - "executor": "@nx/webpack:webpack", + "executor": "nx:run-commands", "defaultConfiguration": "chrome-dev", "options": { - "outputPath": "dist/apps/browser", - "webpackConfig": "apps/browser/webpack.config.js", - "tsConfig": "apps/browser/tsconfig.json", - "main": "apps/browser/src/popup/main.ts", - "target": "web", - "compiler": "tsc", - "watch": true + "cwd": "apps/browser" }, "configurations": { "chrome-dev": { - "mode": "development", - "outputPath": "dist/apps/browser/chrome-dev", - "env": { - "BROWSER": "chrome", - "MANIFEST_VERSION": "3", - "NODE_ENV": "development" - } + "command": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_ENV=development NODE_OPTIONS=\"--max-old-space-size=8192\" webpack --watch --output-path=../../dist/apps/browser/chrome-dev" }, "firefox-dev": { - "mode": "development", - "outputPath": "dist/apps/browser/firefox-dev", - "env": { - "BROWSER": "firefox", - "MANIFEST_VERSION": "3", - "NODE_ENV": "development" - } + "command": "cross-env BROWSER=firefox MANIFEST_VERSION=3 NODE_ENV=development NODE_OPTIONS=\"--max-old-space-size=8192\" webpack --watch --output-path=../../dist/apps/browser/firefox-dev" }, "firefox-mv2-dev": { - "mode": "development", - "outputPath": "dist/apps/browser/firefox-mv2-dev", - "env": { - "BROWSER": "firefox", - "MANIFEST_VERSION": "2", - "NODE_ENV": "development" - } + "command": "cross-env BROWSER=firefox MANIFEST_VERSION=2 NODE_ENV=development NODE_OPTIONS=\"--max-old-space-size=8192\" webpack --watch --output-path=../../dist/apps/browser/firefox-mv2-dev" }, "safari-dev": { - "mode": "development", - "outputPath": "dist/apps/browser/safari-dev", - "env": { - "BROWSER": "safari", - "MANIFEST_VERSION": "3", - "NODE_ENV": "development" - } + "command": "cross-env BROWSER=safari MANIFEST_VERSION=3 NODE_ENV=development NODE_OPTIONS=\"--max-old-space-size=8192\" webpack --watch --output-path=../../dist/apps/browser/safari-dev" }, "safari-mv2-dev": { - "mode": "development", - "outputPath": "dist/apps/browser/safari-mv2-dev", - "env": { - "BROWSER": "safari", - "MANIFEST_VERSION": "2", - "NODE_ENV": "development" - } + "command": "cross-env BROWSER=safari MANIFEST_VERSION=2 NODE_ENV=development NODE_OPTIONS=\"--max-old-space-size=8192\" webpack --watch --output-path=../../dist/apps/browser/safari-mv2-dev" }, "edge-dev": { - "mode": "development", - "outputPath": "dist/apps/browser/edge-dev", - "env": { - "BROWSER": "edge", - "MANIFEST_VERSION": "3", - "NODE_ENV": "development" - } + "command": "cross-env BROWSER=edge MANIFEST_VERSION=3 NODE_ENV=development NODE_OPTIONS=\"--max-old-space-size=8192\" webpack --watch --output-path=../../dist/apps/browser/edge-dev" }, "opera-dev": { - "mode": "development", - "outputPath": "dist/apps/browser/opera-dev", - "env": { - "BROWSER": "opera", - "MANIFEST_VERSION": "3", - "NODE_ENV": "development" - } + "command": "cross-env BROWSER=opera MANIFEST_VERSION=3 NODE_ENV=development NODE_OPTIONS=\"--max-old-space-size=8192\" webpack --watch --output-path=../../dist/apps/browser/opera-dev" }, "commercial-chrome-dev": { - "mode": "development", - "outputPath": "dist/apps/browser/commercial-chrome-dev", - "webpackConfig": "bitwarden_license/bit-browser/webpack.config.js", - "main": "bitwarden_license/bit-browser/src/popup/main.ts", - "tsConfig": "bitwarden_license/bit-browser/tsconfig.json", - "env": { - "BROWSER": "chrome", - "MANIFEST_VERSION": "3", - "NODE_ENV": "development" - } + "command": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_ENV=development NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-browser/webpack.config.js --watch --output-path=../../dist/apps/browser/commercial-chrome-dev" }, "commercial-firefox-dev": { - "mode": "development", - "outputPath": "dist/apps/browser/commercial-firefox-dev", - "webpackConfig": "bitwarden_license/bit-browser/webpack.config.js", - "main": "bitwarden_license/bit-browser/src/popup/main.ts", - "tsConfig": "bitwarden_license/bit-browser/tsconfig.json", - "env": { - "BROWSER": "firefox", - "MANIFEST_VERSION": "3", - "NODE_ENV": "development" - } + "command": "cross-env BROWSER=firefox MANIFEST_VERSION=3 NODE_ENV=development NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-browser/webpack.config.js --watch --output-path=../../dist/apps/browser/commercial-firefox-dev" }, "commercial-firefox-mv2-dev": { - "mode": "development", - "outputPath": "dist/apps/browser/commercial-firefox-mv2-dev", - "webpackConfig": "bitwarden_license/bit-browser/webpack.config.js", - "main": "bitwarden_license/bit-browser/src/popup/main.ts", - "tsConfig": "bitwarden_license/bit-browser/tsconfig.json", - "env": { - "BROWSER": "firefox", - "MANIFEST_VERSION": "2", - "NODE_ENV": "development" - } + "command": "cross-env BROWSER=firefox MANIFEST_VERSION=2 NODE_ENV=development NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-browser/webpack.config.js --watch --output-path=../../dist/apps/browser/commercial-firefox-mv2-dev" }, "commercial-safari-dev": { - "mode": "development", - "outputPath": "dist/apps/browser/commercial-safari-dev", - "webpackConfig": "bitwarden_license/bit-browser/webpack.config.js", - "main": "bitwarden_license/bit-browser/src/popup/main.ts", - "tsConfig": "bitwarden_license/bit-browser/tsconfig.json", - "env": { - "BROWSER": "safari", - "MANIFEST_VERSION": "3", - "NODE_ENV": "development" - } + "command": "cross-env BROWSER=safari MANIFEST_VERSION=3 NODE_ENV=development NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-browser/webpack.config.js --watch --output-path=../../dist/apps/browser/commercial-safari-dev" }, "commercial-safari-mv2-dev": { - "mode": "development", - "outputPath": "dist/apps/browser/commercial-safari-mv2-dev", - "webpackConfig": "bitwarden_license/bit-browser/webpack.config.js", - "main": "bitwarden_license/bit-browser/src/popup/main.ts", - "tsConfig": "bitwarden_license/bit-browser/tsconfig.json", - "env": { - "BROWSER": "safari", - "MANIFEST_VERSION": "2", - "NODE_ENV": "development" - } + "command": "cross-env BROWSER=safari MANIFEST_VERSION=2 NODE_ENV=development NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-browser/webpack.config.js --watch --output-path=../../dist/apps/browser/commercial-safari-mv2-dev" }, "commercial-edge-dev": { - "mode": "development", - "outputPath": "dist/apps/browser/commercial-edge-dev", - "webpackConfig": "bitwarden_license/bit-browser/webpack.config.js", - "main": "bitwarden_license/bit-browser/src/popup/main.ts", - "tsConfig": "bitwarden_license/bit-browser/tsconfig.json", - "env": { - "BROWSER": "edge", - "MANIFEST_VERSION": "3", - "NODE_ENV": "development" - } + "command": "cross-env BROWSER=edge MANIFEST_VERSION=3 NODE_ENV=development NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-browser/webpack.config.js --watch --output-path=../../dist/apps/browser/commercial-edge-dev" }, "commercial-opera-dev": { - "mode": "development", - "outputPath": "dist/apps/browser/commercial-opera-dev", - "webpackConfig": "bitwarden_license/bit-browser/webpack.config.js", - "main": "bitwarden_license/bit-browser/src/popup/main.ts", - "tsConfig": "bitwarden_license/bit-browser/tsconfig.json", - "env": { - "BROWSER": "opera", - "MANIFEST_VERSION": "3", - "NODE_ENV": "development" - } + "command": "cross-env BROWSER=opera MANIFEST_VERSION=3 NODE_ENV=development NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-browser/webpack.config.js --watch --output-path=../../dist/apps/browser/commercial-opera-dev" } } },