From cbb7e1840d54882fa67530b04adb0600b852ff79 Mon Sep 17 00:00:00 2001
From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
Date: Tue, 16 Apr 2024 19:39:31 +0200
Subject: [PATCH 01/12] [PM-2570] [PM-4649] Update change master password UI
(#8416)
* Update the change master password dialog on browser
Change text to remove the mention of the bitwarden.com web vault
Change icon to show it's external link
Changes based on Figma attached to PM-2570
* Update the change master password dialog on desktop
Change text to remove the mention of the bitwarden.com web vault
Changes based on Figma attached to PM-2570 and to replicate what is done on browser
---------
Co-authored-by: Daniel James Smith
---
apps/browser/src/_locales/en/messages.json | 12 ++++++------
.../src/popup/settings/settings.component.html | 2 +-
.../browser/src/popup/settings/settings.component.ts | 5 +++--
apps/desktop/src/locales/en/messages.json | 7 +++++--
apps/desktop/src/main/menu/menu.account.ts | 8 ++++----
5 files changed, 19 insertions(+), 15 deletions(-)
diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index 4108db3996..5e941083df 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -172,6 +172,12 @@
"changeMasterPassword": {
"message": "Change master password"
},
+ "continueToWebApp": {
+ "message": "Continue to web app?"
+ },
+ "changeMasterPasswordOnWebConfirmation": {
+ "message": "You can change your master password on the Bitwarden web app."
+ },
"fingerprintPhrase": {
"message": "Fingerprint phrase",
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
@@ -557,12 +563,6 @@
"addedFolder": {
"message": "Folder added"
},
- "changeMasterPass": {
- "message": "Change master password"
- },
- "changeMasterPasswordConfirmation": {
- "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?"
- },
"twoStepLoginConfirmation": {
"message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?"
},
diff --git a/apps/browser/src/popup/settings/settings.component.html b/apps/browser/src/popup/settings/settings.component.html
index f099528918..98c218b0db 100644
--- a/apps/browser/src/popup/settings/settings.component.html
+++ b/apps/browser/src/popup/settings/settings.component.html
@@ -153,7 +153,7 @@
*ngIf="showChangeMasterPass"
>
{{ "changeMasterPassword" | i18n }}
-
+
{
const result = await dialog.showMessageBox(this._window, {
- title: this.localize("changeMasterPass"),
- message: this.localize("changeMasterPass"),
- detail: this.localize("changeMasterPasswordConfirmation"),
- buttons: [this.localize("yes"), this.localize("no")],
+ title: this.localize("continueToWebApp"),
+ message: this.localize("continueToWebApp"),
+ detail: this.localize("changeMasterPasswordOnWebConfirmation"),
+ buttons: [this.localize("continue"), this.localize("cancel")],
cancelId: 1,
defaultId: 0,
noLink: true,
From 51a6b34cc29dd3316c89c7fa706d263cbc127c9c Mon Sep 17 00:00:00 2001
From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
Date: Tue, 16 Apr 2024 14:05:47 -0400
Subject: [PATCH 02/12] Auth/PM-7467- Fix Refresh token issues (#8757)
* PM-7467 - Login Strategy bug - VaultTimeoutSettings will be undefined before the account is activated unless you pass in user ids to retrieve the data. This resulted in refresh tokens always being set into secure storage regardless of a user's vault timeout settings (logout should translate to memory)
* PM-7467 - TokenSvc - Fix bug in getRefreshToken which would retrieve the user's refresh token from secure storage even if the user had changed their vault timeout setting to log out which moved the refresh token into memory. Includes a migration to remove the no longer required REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE state provider flag.
* PM-7467 - Per PR feedback, use IRREVERSIBLE for rollback.
Co-authored-by: Jake Fink
* PM-7467 - fix tests
* PM-7467 - Fix migrator based on PR feedback.
* PM-7467 - Bump migration version
---------
Co-authored-by: Jake Fink
---
.../common/login-strategies/login.strategy.ts | 4 +-
.../src/auth/services/token.service.spec.ts | 30 +-------
.../common/src/auth/services/token.service.ts | 35 ++++-----
.../src/auth/services/token.state.spec.ts | 2 -
libs/common/src/auth/services/token.state.ts | 9 ---
libs/common/src/state-migrations/migrate.ts | 6 +-
...token-migrated-state-provider-flag.spec.ts | 72 +++++++++++++++++++
...resh-token-migrated-state-provider-flag.ts | 34 +++++++++
8 files changed, 125 insertions(+), 67 deletions(-)
create mode 100644 libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.spec.ts
create mode 100644 libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.ts
diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts
index 94f96d40d0..a6dc193183 100644
--- a/libs/auth/src/common/login-strategies/login.strategy.ts
+++ b/libs/auth/src/common/login-strategies/login.strategy.ts
@@ -166,8 +166,8 @@ export abstract class LoginStrategy {
const userId = accountInformation.sub;
- const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction();
- const vaultTimeout = await this.stateService.getVaultTimeout();
+ const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId });
+ const vaultTimeout = await this.stateService.getVaultTimeout({ userId });
// set access token and refresh token before account initialization so authN status can be accurate
// User id will be derived from the access token.
diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts
index c409263209..d32c4d8e1c 100644
--- a/libs/common/src/auth/services/token.service.spec.ts
+++ b/libs/common/src/auth/services/token.service.spec.ts
@@ -23,7 +23,6 @@ import {
EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL,
REFRESH_TOKEN_DISK,
REFRESH_TOKEN_MEMORY,
- REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE,
} from "./token.state";
describe("TokenService", () => {
@@ -1120,20 +1119,13 @@ describe("TokenService", () => {
secureStorageOptions,
);
- // assert data was migrated out of disk and memory + flag was set
+ // assert data was migrated out of disk and memory
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK).nextMock,
).toHaveBeenCalledWith(null);
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY).nextMock,
).toHaveBeenCalledWith(null);
-
- expect(
- singleUserStateProvider.getFake(
- userIdFromAccessToken,
- REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE,
- ).nextMock,
- ).toHaveBeenCalledWith(true);
});
});
});
@@ -1260,11 +1252,6 @@ describe("TokenService", () => {
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
- // set access token migration flag to true
- singleUserStateProvider
- .getFake(userIdFromAccessToken, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE)
- .stateSubject.next([userIdFromAccessToken, true]);
-
// Act
const result = await tokenService.getRefreshToken();
// Assert
@@ -1284,11 +1271,6 @@ describe("TokenService", () => {
secureStorageService.get.mockResolvedValue(refreshToken);
- // set access token migration flag to true
- singleUserStateProvider
- .getFake(userIdFromAccessToken, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE)
- .stateSubject.next([userIdFromAccessToken, true]);
-
// Act
const result = await tokenService.getRefreshToken(userIdFromAccessToken);
// Assert
@@ -1305,11 +1287,6 @@ describe("TokenService", () => {
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
- // set refresh token migration flag to false
- singleUserStateProvider
- .getFake(userIdFromAccessToken, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE)
- .stateSubject.next([userIdFromAccessToken, false]);
-
// Act
const result = await tokenService.getRefreshToken(userIdFromAccessToken);
@@ -1335,11 +1312,6 @@ describe("TokenService", () => {
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
- // set access token migration flag to false
- singleUserStateProvider
- .getFake(userIdFromAccessToken, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE)
- .stateSubject.next([userIdFromAccessToken, false]);
-
// Act
const result = await tokenService.getRefreshToken();
diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts
index db39997663..c24a2c186b 100644
--- a/libs/common/src/auth/services/token.service.ts
+++ b/libs/common/src/auth/services/token.service.ts
@@ -32,7 +32,6 @@ import {
EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL,
REFRESH_TOKEN_DISK,
REFRESH_TOKEN_MEMORY,
- REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE,
} from "./token.state";
export enum TokenStorageLocation {
@@ -441,9 +440,6 @@ export class TokenService implements TokenServiceAbstraction {
await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_DISK).update((_) => null);
await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_MEMORY).update((_) => null);
- // Set flag to indicate that the refresh token has been migrated to secure storage (don't remove this)
- await this.setRefreshTokenMigratedToSecureStorage(userId);
-
return;
case TokenStorageLocation.Disk:
@@ -467,12 +463,6 @@ export class TokenService implements TokenServiceAbstraction {
return undefined;
}
- const refreshTokenMigratedToSecureStorage =
- await this.getRefreshTokenMigratedToSecureStorage(userId);
- if (this.platformSupportsSecureStorage && refreshTokenMigratedToSecureStorage) {
- return await this.getStringFromSecureStorage(userId, this.refreshTokenSecureStorageKey);
- }
-
// pre-secure storage migration:
// Always read memory first b/c faster
const refreshTokenMemory = await this.getStateValueByUserIdAndKeyDef(
@@ -484,13 +474,24 @@ export class TokenService implements TokenServiceAbstraction {
return refreshTokenMemory;
}
- // if memory is null, read from disk
+ // if memory is null, read from disk and then secure storage
const refreshTokenDisk = await this.getStateValueByUserIdAndKeyDef(userId, REFRESH_TOKEN_DISK);
if (refreshTokenDisk != null) {
return refreshTokenDisk;
}
+ if (this.platformSupportsSecureStorage) {
+ const refreshTokenSecureStorage = await this.getStringFromSecureStorage(
+ userId,
+ this.refreshTokenSecureStorageKey,
+ );
+
+ if (refreshTokenSecureStorage != null) {
+ return refreshTokenSecureStorage;
+ }
+ }
+
return null;
}
@@ -516,18 +517,6 @@ export class TokenService implements TokenServiceAbstraction {
await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_DISK).update((_) => null);
}
- private async getRefreshTokenMigratedToSecureStorage(userId: UserId): Promise {
- return await firstValueFrom(
- this.singleUserStateProvider.get(userId, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE).state$,
- );
- }
-
- private async setRefreshTokenMigratedToSecureStorage(userId: UserId): Promise {
- await this.singleUserStateProvider
- .get(userId, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE)
- .update((_) => true);
- }
-
async setClientId(
clientId: string,
vaultTimeoutAction: VaultTimeoutAction,
diff --git a/libs/common/src/auth/services/token.state.spec.ts b/libs/common/src/auth/services/token.state.spec.ts
index 55f97b7e00..dc00fec383 100644
--- a/libs/common/src/auth/services/token.state.spec.ts
+++ b/libs/common/src/auth/services/token.state.spec.ts
@@ -10,7 +10,6 @@ import {
EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL,
REFRESH_TOKEN_DISK,
REFRESH_TOKEN_MEMORY,
- REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE,
} from "./token.state";
describe.each([
@@ -18,7 +17,6 @@ describe.each([
[ACCESS_TOKEN_MEMORY, "accessTokenMemory"],
[REFRESH_TOKEN_DISK, "refreshTokenDisk"],
[REFRESH_TOKEN_MEMORY, "refreshTokenMemory"],
- [REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, true],
[EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, { user: "token" }],
[API_KEY_CLIENT_ID_DISK, "apiKeyClientIdDisk"],
[API_KEY_CLIENT_ID_MEMORY, "apiKeyClientIdMemory"],
diff --git a/libs/common/src/auth/services/token.state.ts b/libs/common/src/auth/services/token.state.ts
index a8c6878fbb..458d6846c1 100644
--- a/libs/common/src/auth/services/token.state.ts
+++ b/libs/common/src/auth/services/token.state.ts
@@ -30,15 +30,6 @@ export const REFRESH_TOKEN_MEMORY = new UserKeyDefinition(TOKEN_MEMORY,
clearOn: [], // Manually handled
});
-export const REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE = new UserKeyDefinition(
- TOKEN_DISK,
- "refreshTokenMigratedToSecureStorage",
- {
- deserializer: (refreshTokenMigratedToSecureStorage) => refreshTokenMigratedToSecureStorage,
- clearOn: [], // Don't clear on lock/logout so that we always check the correct place (secure storage) for the refresh token if it's been migrated
- },
-);
-
export const EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL = KeyDefinition.record(
TOKEN_DISK_LOCAL,
"emailTwoFactorTokenRecord",
diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts
index 000f85519e..f9a8734731 100644
--- a/libs/common/src/state-migrations/migrate.ts
+++ b/libs/common/src/state-migrations/migrate.ts
@@ -54,6 +54,7 @@ import { SendMigrator } from "./migrations/54-move-encrypted-sends";
import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-master-key-state-to-provider";
import { AuthRequestMigrator } from "./migrations/56-move-auth-requests";
import { CipherServiceMigrator } from "./migrations/57-move-cipher-service-to-state-provider";
+import { RemoveRefreshTokenMigratedFlagMigrator } from "./migrations/58-remove-refresh-token-migrated-state-provider-flag";
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
@@ -61,7 +62,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 3;
-export const CURRENT_VERSION = 57;
+export const CURRENT_VERSION = 58;
export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() {
@@ -120,7 +121,8 @@ export function createMigrationBuilder() {
.with(SendMigrator, 53, 54)
.with(MoveMasterKeyStateToProviderMigrator, 54, 55)
.with(AuthRequestMigrator, 55, 56)
- .with(CipherServiceMigrator, 56, CURRENT_VERSION);
+ .with(CipherServiceMigrator, 56, 57)
+ .with(RemoveRefreshTokenMigratedFlagMigrator, 57, CURRENT_VERSION);
}
export async function currentVersion(
diff --git a/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.spec.ts b/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.spec.ts
new file mode 100644
index 0000000000..8d8040e4a0
--- /dev/null
+++ b/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.spec.ts
@@ -0,0 +1,72 @@
+import { MockProxy, any } from "jest-mock-extended";
+
+import { MigrationHelper } from "../migration-helper";
+import { mockMigrationHelper } from "../migration-helper.spec";
+import { IRREVERSIBLE } from "../migrator";
+
+import {
+ REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE,
+ RemoveRefreshTokenMigratedFlagMigrator,
+} from "./58-remove-refresh-token-migrated-state-provider-flag";
+
+// Represents data in state service pre-migration
+function preMigrationJson() {
+ return {
+ global: {
+ otherStuff: "otherStuff1",
+ },
+ authenticatedAccounts: ["user1", "user2", "user3"],
+
+ user_user1_token_refreshTokenMigratedToSecureStorage: true,
+ user_user2_token_refreshTokenMigratedToSecureStorage: false,
+ };
+}
+
+function rollbackJSON() {
+ return {
+ global: {
+ otherStuff: "otherStuff1",
+ },
+ authenticatedAccounts: ["user1", "user2", "user3"],
+ };
+}
+
+describe("RemoveRefreshTokenMigratedFlagMigrator", () => {
+ let helper: MockProxy;
+ let sut: RemoveRefreshTokenMigratedFlagMigrator;
+
+ describe("migrate", () => {
+ beforeEach(() => {
+ helper = mockMigrationHelper(preMigrationJson(), 57);
+ sut = new RemoveRefreshTokenMigratedFlagMigrator(57, 58);
+ });
+
+ it("should remove refreshTokenMigratedToSecureStorage from state provider for all accounts that have it", async () => {
+ await sut.migrate(helper);
+
+ expect(helper.removeFromUser).toHaveBeenCalledWith(
+ "user1",
+ REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE,
+ );
+ expect(helper.removeFromUser).toHaveBeenCalledWith(
+ "user2",
+ REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE,
+ );
+
+ expect(helper.removeFromUser).toHaveBeenCalledTimes(2);
+
+ expect(helper.removeFromUser).not.toHaveBeenCalledWith("user3", any());
+ });
+ });
+
+ describe("rollback", () => {
+ beforeEach(() => {
+ helper = mockMigrationHelper(rollbackJSON(), 58);
+ sut = new RemoveRefreshTokenMigratedFlagMigrator(57, 58);
+ });
+
+ it("should not add data back and throw IRREVERSIBLE error on call", async () => {
+ await expect(sut.rollback(helper)).rejects.toThrow(IRREVERSIBLE);
+ });
+ });
+});
diff --git a/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.ts b/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.ts
new file mode 100644
index 0000000000..9c6d3776fe
--- /dev/null
+++ b/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.ts
@@ -0,0 +1,34 @@
+import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
+import { IRREVERSIBLE, Migrator } from "../migrator";
+
+type ExpectedAccountType = NonNullable;
+
+export const REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE: KeyDefinitionLike = {
+ key: "refreshTokenMigratedToSecureStorage", // matches KeyDefinition.key in DeviceTrustCryptoService
+ stateDefinition: {
+ name: "token", // matches StateDefinition.name in StateDefinitions
+ },
+};
+
+export class RemoveRefreshTokenMigratedFlagMigrator extends Migrator<57, 58> {
+ async migrate(helper: MigrationHelper): Promise {
+ const accounts = await helper.getAccounts();
+ async function migrateAccount(userId: string, account: ExpectedAccountType): Promise {
+ const refreshTokenMigratedFlag = await helper.getFromUser(
+ userId,
+ REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE,
+ );
+
+ if (refreshTokenMigratedFlag != null) {
+ // Only delete the flag if it exists
+ await helper.removeFromUser(userId, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE);
+ }
+ }
+
+ await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
+ }
+
+ async rollback(helper: MigrationHelper): Promise {
+ throw IRREVERSIBLE;
+ }
+}
From 3c2d3669c56f0d6055732453d75b1ed496e4741b Mon Sep 17 00:00:00 2001
From: Bitwarden DevOps
<106330231+bitwarden-devops-bot@users.noreply.github.com>
Date: Tue, 16 Apr 2024 15:26:30 -0400
Subject: [PATCH 03/12] Bumped web version to (#8772)
---
apps/web/package.json | 2 +-
package-lock.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/web/package.json b/apps/web/package.json
index 99828bb543..55fe0987d7 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@bitwarden/web-vault",
- "version": "2024.4.0",
+ "version": "2024.4.1",
"scripts": {
"build:oss": "webpack",
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
diff --git a/package-lock.json b/package-lock.json
index c399536cca..096f6653cb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -247,7 +247,7 @@
},
"apps/web": {
"name": "@bitwarden/web-vault",
- "version": "2024.4.0"
+ "version": "2024.4.1"
},
"libs/admin-console": {
"name": "@bitwarden/admin-console",
From d6f2965367dbf583350b3bbe19b06cc109fd5665 Mon Sep 17 00:00:00 2001
From: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com>
Date: Wed, 17 Apr 2024 01:07:47 +0530
Subject: [PATCH 04/12] [PM-5015] org billing history view component migration
(#8302)
---
.../organization-billing-history-view.component.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html
index 9df05d02ed..087009b291 100644
--- a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html
+++ b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html
@@ -16,11 +16,11 @@
- {{ "loading" | i18n }}
+ {{ "loading" | i18n }}
From f5198e86fd8a2187e3c9e528ba788edf2595fa2d Mon Sep 17 00:00:00 2001
From: KiruthigaManivannan
<162679756+KiruthigaManivannan@users.noreply.github.com>
Date: Wed, 17 Apr 2024 01:08:19 +0530
Subject: [PATCH 05/12] PM-5019 Migrate Adjust Payment component (#8383)
* PM-5019 Migrated Adjust Payment component
* PM-5019 Migrated Adjust Payment dialog component
* PM-5019 Removing type any
* PM-5019 Addressed review comments
* PM-5019 Included deleted line space
---
.../adjust-payment-dialog.component.html | 25 ++++
.../shared/adjust-payment-dialog.component.ts | 110 ++++++++++++++++++
.../shared/adjust-payment.component.html | 19 ---
.../shared/adjust-payment.component.ts | 90 --------------
.../billing/shared/billing-shared.module.ts | 4 +-
.../shared/payment-method.component.html | 18 +--
.../shared/payment-method.component.ts | 28 +++--
7 files changed, 155 insertions(+), 139 deletions(-)
create mode 100644 apps/web/src/app/billing/shared/adjust-payment-dialog.component.html
create mode 100644 apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts
delete mode 100644 apps/web/src/app/billing/shared/adjust-payment.component.html
delete mode 100644 apps/web/src/app/billing/shared/adjust-payment.component.ts
diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog.component.html b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.html
new file mode 100644
index 0000000000..0f92b023b1
--- /dev/null
+++ b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.html
@@ -0,0 +1,25 @@
+
diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts
new file mode 100644
index 0000000000..41d0ad7e7a
--- /dev/null
+++ b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts
@@ -0,0 +1,110 @@
+import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
+import { Component, Inject, ViewChild } from "@angular/core";
+import { FormGroup } from "@angular/forms";
+
+import { ApiService } from "@bitwarden/common/abstractions/api.service";
+import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
+import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
+import { PaymentMethodType } from "@bitwarden/common/billing/enums";
+import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
+import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
+import { DialogService } from "@bitwarden/components";
+
+import { PaymentComponent } from "./payment.component";
+import { TaxInfoComponent } from "./tax-info.component";
+
+export interface AdjustPaymentDialogData {
+ organizationId: string;
+ currentType: PaymentMethodType;
+}
+
+export enum AdjustPaymentDialogResult {
+ Adjusted = "adjusted",
+ Cancelled = "cancelled",
+}
+
+@Component({
+ templateUrl: "adjust-payment-dialog.component.html",
+})
+export class AdjustPaymentDialogComponent {
+ @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
+ @ViewChild(TaxInfoComponent, { static: true }) taxInfoComponent: TaxInfoComponent;
+
+ organizationId: string;
+ currentType: PaymentMethodType;
+ paymentMethodType = PaymentMethodType;
+
+ protected DialogResult = AdjustPaymentDialogResult;
+ protected formGroup = new FormGroup({});
+
+ constructor(
+ private dialogRef: DialogRef,
+ @Inject(DIALOG_DATA) protected data: AdjustPaymentDialogData,
+ private apiService: ApiService,
+ private i18nService: I18nService,
+ private platformUtilsService: PlatformUtilsService,
+ private logService: LogService,
+ private organizationApiService: OrganizationApiServiceAbstraction,
+ private paymentMethodWarningService: PaymentMethodWarningService,
+ ) {
+ this.organizationId = data.organizationId;
+ this.currentType = data.currentType;
+ }
+
+ submit = async () => {
+ const request = new PaymentRequest();
+ const response = this.paymentComponent.createPaymentToken().then((result) => {
+ request.paymentToken = result[0];
+ request.paymentMethodType = result[1];
+ request.postalCode = this.taxInfoComponent.taxInfo.postalCode;
+ request.country = this.taxInfoComponent.taxInfo.country;
+ if (this.organizationId == null) {
+ return this.apiService.postAccountPayment(request);
+ } else {
+ request.taxId = this.taxInfoComponent.taxInfo.taxId;
+ request.state = this.taxInfoComponent.taxInfo.state;
+ request.line1 = this.taxInfoComponent.taxInfo.line1;
+ request.line2 = this.taxInfoComponent.taxInfo.line2;
+ request.city = this.taxInfoComponent.taxInfo.city;
+ request.state = this.taxInfoComponent.taxInfo.state;
+ return this.organizationApiService.updatePayment(this.organizationId, request);
+ }
+ });
+ await response;
+ if (this.organizationId) {
+ await this.paymentMethodWarningService.removeSubscriptionRisk(this.organizationId);
+ }
+ this.platformUtilsService.showToast(
+ "success",
+ null,
+ this.i18nService.t("updatedPaymentMethod"),
+ );
+ this.dialogRef.close(AdjustPaymentDialogResult.Adjusted);
+ };
+
+ changeCountry() {
+ if (this.taxInfoComponent.taxInfo.country === "US") {
+ this.paymentComponent.hideBank = !this.organizationId;
+ } else {
+ this.paymentComponent.hideBank = true;
+ if (this.paymentComponent.method === PaymentMethodType.BankAccount) {
+ this.paymentComponent.method = PaymentMethodType.Card;
+ this.paymentComponent.changeMethod();
+ }
+ }
+ }
+}
+
+/**
+ * Strongly typed helper to open a AdjustPaymentDialog
+ * @param dialogService Instance of the dialog service that will be used to open the dialog
+ * @param config Configuration for the dialog
+ */
+export function openAdjustPaymentDialog(
+ dialogService: DialogService,
+ config: DialogConfig,
+) {
+ return dialogService.open(AdjustPaymentDialogComponent, config);
+}
diff --git a/apps/web/src/app/billing/shared/adjust-payment.component.html b/apps/web/src/app/billing/shared/adjust-payment.component.html
deleted file mode 100644
index 724e7a44c2..0000000000
--- a/apps/web/src/app/billing/shared/adjust-payment.component.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
diff --git a/apps/web/src/app/billing/shared/adjust-payment.component.ts b/apps/web/src/app/billing/shared/adjust-payment.component.ts
deleted file mode 100644
index 7452344141..0000000000
--- a/apps/web/src/app/billing/shared/adjust-payment.component.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import { Component, EventEmitter, Input, Output, ViewChild } from "@angular/core";
-
-import { ApiService } from "@bitwarden/common/abstractions/api.service";
-import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
-import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
-import { PaymentMethodType } from "@bitwarden/common/billing/enums";
-import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
-import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
-import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
-import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
-
-import { PaymentComponent } from "./payment.component";
-import { TaxInfoComponent } from "./tax-info.component";
-
-@Component({
- selector: "app-adjust-payment",
- templateUrl: "adjust-payment.component.html",
-})
-export class AdjustPaymentComponent {
- @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
- @ViewChild(TaxInfoComponent, { static: true }) taxInfoComponent: TaxInfoComponent;
-
- @Input() currentType?: PaymentMethodType;
- @Input() organizationId: string;
- @Output() onAdjusted = new EventEmitter();
- @Output() onCanceled = new EventEmitter();
-
- paymentMethodType = PaymentMethodType;
- formPromise: Promise;
-
- constructor(
- private apiService: ApiService,
- private i18nService: I18nService,
- private platformUtilsService: PlatformUtilsService,
- private logService: LogService,
- private organizationApiService: OrganizationApiServiceAbstraction,
- private paymentMethodWarningService: PaymentMethodWarningService,
- ) {}
-
- async submit() {
- try {
- const request = new PaymentRequest();
- this.formPromise = this.paymentComponent.createPaymentToken().then((result) => {
- request.paymentToken = result[0];
- request.paymentMethodType = result[1];
- request.postalCode = this.taxInfoComponent.taxInfo.postalCode;
- request.country = this.taxInfoComponent.taxInfo.country;
- if (this.organizationId == null) {
- return this.apiService.postAccountPayment(request);
- } else {
- request.taxId = this.taxInfoComponent.taxInfo.taxId;
- request.state = this.taxInfoComponent.taxInfo.state;
- request.line1 = this.taxInfoComponent.taxInfo.line1;
- request.line2 = this.taxInfoComponent.taxInfo.line2;
- request.city = this.taxInfoComponent.taxInfo.city;
- request.state = this.taxInfoComponent.taxInfo.state;
- return this.organizationApiService.updatePayment(this.organizationId, request);
- }
- });
- await this.formPromise;
- if (this.organizationId) {
- await this.paymentMethodWarningService.removeSubscriptionRisk(this.organizationId);
- }
- this.platformUtilsService.showToast(
- "success",
- null,
- this.i18nService.t("updatedPaymentMethod"),
- );
- this.onAdjusted.emit();
- } catch (e) {
- this.logService.error(e);
- }
- }
-
- cancel() {
- this.onCanceled.emit();
- }
-
- changeCountry() {
- if (this.taxInfoComponent.taxInfo.country === "US") {
- this.paymentComponent.hideBank = !this.organizationId;
- } else {
- this.paymentComponent.hideBank = true;
- if (this.paymentComponent.method === PaymentMethodType.BankAccount) {
- this.paymentComponent.method = PaymentMethodType.Card;
- this.paymentComponent.changeMethod();
- }
- }
- }
-}
diff --git a/apps/web/src/app/billing/shared/billing-shared.module.ts b/apps/web/src/app/billing/shared/billing-shared.module.ts
index 2f773870aa..65a651b73d 100644
--- a/apps/web/src/app/billing/shared/billing-shared.module.ts
+++ b/apps/web/src/app/billing/shared/billing-shared.module.ts
@@ -4,7 +4,7 @@ import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
import { AddCreditComponent } from "./add-credit.component";
-import { AdjustPaymentComponent } from "./adjust-payment.component";
+import { AdjustPaymentDialogComponent } from "./adjust-payment-dialog.component";
import { AdjustStorageComponent } from "./adjust-storage.component";
import { BillingHistoryComponent } from "./billing-history.component";
import { OffboardingSurveyComponent } from "./offboarding-survey.component";
@@ -18,7 +18,7 @@ import { UpdateLicenseComponent } from "./update-license.component";
imports: [SharedModule, PaymentComponent, TaxInfoComponent, HeaderModule],
declarations: [
AddCreditComponent,
- AdjustPaymentComponent,
+ AdjustPaymentDialogComponent,
AdjustStorageComponent,
BillingHistoryComponent,
PaymentMethodComponent,
diff --git a/apps/web/src/app/billing/shared/payment-method.component.html b/apps/web/src/app/billing/shared/payment-method.component.html
index cfe98178b0..5f78294fa6 100644
--- a/apps/web/src/app/billing/shared/payment-method.component.html
+++ b/apps/web/src/app/billing/shared/payment-method.component.html
@@ -15,7 +15,7 @@
@@ -102,23 +102,9 @@
{{ paymentSource.description }}
-
+
{{ (paymentSource ? "changePaymentMethod" : "addPaymentMethod") | i18n }}
-
-
{{ "paymentChargedWithUnpaidSubscription" | i18n }}
diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts
index d2b65968c3..fee97cb912 100644
--- a/apps/web/src/app/billing/shared/payment-method.component.ts
+++ b/apps/web/src/app/billing/shared/payment-method.component.ts
@@ -1,6 +1,7 @@
import { Component, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, FormControl, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
+import { lastValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
@@ -14,6 +15,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components";
+import {
+ AdjustPaymentDialogResult,
+ openAdjustPaymentDialog,
+} from "./adjust-payment-dialog.component";
import { TaxInfoComponent } from "./tax-info.component";
@Component({
@@ -25,7 +30,6 @@ export class PaymentMethodComponent implements OnInit {
loading = false;
firstLoaded = false;
- showAdjustPayment = false;
showAddCredit = false;
billing: BillingPaymentResponse;
org: OrganizationSubscriptionResponse;
@@ -120,18 +124,18 @@ export class PaymentMethodComponent implements OnInit {
}
}
- changePayment() {
- this.showAdjustPayment = true;
- }
-
- closePayment(load: boolean) {
- this.showAdjustPayment = false;
- if (load) {
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.load();
+ changePayment = async () => {
+ const dialogRef = openAdjustPaymentDialog(this.dialogService, {
+ data: {
+ organizationId: this.organizationId,
+ currentType: this.paymentSource !== null ? this.paymentSource.type : null,
+ },
+ });
+ const result = await lastValueFrom(dialogRef.closed);
+ if (result === AdjustPaymentDialogResult.Adjusted) {
+ await this.load();
}
- }
+ };
async verifyBank() {
if (this.loading || !this.forOrganization) {
From 5d3541dd6379f422f0f740c3702367b58c8898f8 Mon Sep 17 00:00:00 2001
From: vinith-kovan <156108204+vinith-kovan@users.noreply.github.com>
Date: Wed, 17 Apr 2024 01:09:05 +0530
Subject: [PATCH 06/12] [PM-5013] migrate change plan component (#8407)
* change plan component migration
* change plan component migration
---------
Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com>
---
.../organizations/change-plan.component.html | 22 +++++++++++++------
1 file changed, 15 insertions(+), 7 deletions(-)
diff --git a/apps/web/src/app/billing/organizations/change-plan.component.html b/apps/web/src/app/billing/organizations/change-plan.component.html
index b9a15be5ea..a25dde4fd3 100644
--- a/apps/web/src/app/billing/organizations/change-plan.component.html
+++ b/apps/web/src/app/billing/organizations/change-plan.component.html
@@ -1,10 +1,18 @@
-
-
-
- ×
-
-
{{ "changeBillingPlan" | i18n }}
-
{{ "changeBillingPlanUpgrade" | i18n }}
+
+
+
+
{{ "changeBillingPlan" | i18n }}
+
{{ "changeBillingPlanUpgrade" | i18n }}
Date: Wed, 17 Apr 2024 01:09:53 +0530
Subject: [PATCH 07/12] [PM-5021] billing history component migration (#8042)
* billing history component migration
* billing history component migration
* billing history component migration
* billing history component migration
* billing history component migration
---------
Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com>
---
.../billing-history-view.component.html | 11 +-
.../shared/billing-history.component.html | 135 +++++++++---------
2 files changed, 76 insertions(+), 70 deletions(-)
diff --git a/apps/web/src/app/billing/individual/billing-history-view.component.html b/apps/web/src/app/billing/individual/billing-history-view.component.html
index 1032558f5f..2491fc42c7 100644
--- a/apps/web/src/app/billing/individual/billing-history-view.component.html
+++ b/apps/web/src/app/billing/individual/billing-history-view.component.html
@@ -1,13 +1,12 @@
-
-
-
+
+
{{ "addStorage" | i18n }}
{{ "removeStorage" | i18n }}
-
diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts
index 7d8c3a0f18..fa21317c18 100644
--- a/apps/web/src/app/billing/individual/user-subscription.component.ts
+++ b/apps/web/src/app/billing/individual/user-subscription.component.ts
@@ -12,6 +12,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components";
+import {
+ AdjustStorageDialogResult,
+ openAdjustStorageDialog,
+} from "../shared/adjust-storage.component";
import {
OffboardingSurveyDialogResultType,
openOffboardingSurvey,
@@ -24,7 +28,6 @@ export class UserSubscriptionComponent implements OnInit {
loading = false;
firstLoaded = false;
adjustStorageAdd = true;
- showAdjustStorage = false;
showUpdateLicense = false;
sub: SubscriptionResponse;
selfHosted = false;
@@ -144,19 +147,20 @@ export class UserSubscriptionComponent implements OnInit {
}
}
- adjustStorage(add: boolean) {
- this.adjustStorageAdd = add;
- this.showAdjustStorage = true;
- }
-
- closeStorage(load: boolean) {
- this.showAdjustStorage = false;
- if (load) {
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.load();
- }
- }
+ adjustStorage = (add: boolean) => {
+ return async () => {
+ const dialogRef = openAdjustStorageDialog(this.dialogService, {
+ data: {
+ storageGbPrice: 4,
+ add: add,
+ },
+ });
+ const result = await lastValueFrom(dialogRef.closed);
+ if (result === AdjustStorageDialogResult.Adjusted) {
+ await this.load();
+ }
+ };
+ };
get subscriptionMarkedForCancel() {
return (
diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html
index b4fac65854..16641c0d52 100644
--- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html
+++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html
@@ -175,23 +175,24 @@
-
-
+
+
{{ "addStorage" | i18n }}
-
+
{{ "removeStorage" | i18n }}
-
diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts
index 2173d4c0ca..9326359bd8 100644
--- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts
+++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts
@@ -18,6 +18,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components";
+import {
+ AdjustStorageDialogResult,
+ openAdjustStorageDialog,
+} from "../shared/adjust-storage.component";
import {
OffboardingSurveyDialogResultType,
openOffboardingSurvey,
@@ -36,8 +40,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
userOrg: Organization;
showChangePlan = false;
showDownloadLicense = false;
- adjustStorageAdd = true;
- showAdjustStorage = false;
hasBillingSyncToken: boolean;
showAdjustSecretsManager = false;
showSecretsManagerSubscribe = false;
@@ -361,19 +363,22 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
this.load();
}
- adjustStorage(add: boolean) {
- this.adjustStorageAdd = add;
- this.showAdjustStorage = true;
- }
-
- closeStorage(load: boolean) {
- this.showAdjustStorage = false;
- if (load) {
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.load();
- }
- }
+ adjustStorage = (add: boolean) => {
+ return async () => {
+ const dialogRef = openAdjustStorageDialog(this.dialogService, {
+ data: {
+ storageGbPrice: this.storageGbPrice,
+ add: add,
+ organizationId: this.organizationId,
+ interval: this.billingInterval,
+ },
+ });
+ const result = await lastValueFrom(dialogRef.closed);
+ if (result === AdjustStorageDialogResult.Adjusted) {
+ await this.load();
+ }
+ };
+ };
removeSponsorship = async () => {
const confirmed = await this.dialogService.openSimpleDialog({
diff --git a/apps/web/src/app/billing/shared/adjust-storage.component.html b/apps/web/src/app/billing/shared/adjust-storage.component.html
index aa6daca335..a597a3ae5e 100644
--- a/apps/web/src/app/billing/shared/adjust-storage.component.html
+++ b/apps/web/src/app/billing/shared/adjust-storage.component.html
@@ -1,43 +1,35 @@
-