From 2a1d9b7f31e83ed7d14bbeecc952fdceffd21ef2 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:20:23 -0400 Subject: [PATCH 01/12] [AC-2963] Add premium-v2.component for individual users (#10885) * Add premium-v2.component * (No Logic) Move existing premium.component into new premium folder * Add new premium-v2.component to /premium route behind FF --- .../individual-billing-routing.module.ts | 20 ++- .../individual/individual-billing.module.ts | 4 +- .../premium/premium-v2.component.html | 144 +++++++++++++++ .../premium/premium-v2.component.ts | 164 ++++++++++++++++++ .../{ => premium}/premium.component.html | 0 .../{ => premium}/premium.component.ts | 2 +- .../billing/shared/billing-shared.module.ts | 1 + 7 files changed, 327 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/app/billing/individual/premium/premium-v2.component.html create mode 100644 apps/web/src/app/billing/individual/premium/premium-v2.component.ts rename apps/web/src/app/billing/individual/{ => premium}/premium.component.html (100%) rename apps/web/src/app/billing/individual/{ => premium}/premium.component.ts (98%) 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 ff45ca75ac6..585d9b418c1 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,10 +1,14 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; + import { PaymentMethodComponent } from "../shared"; import { BillingHistoryViewComponent } from "./billing-history-view.component"; -import { PremiumComponent } from "./premium.component"; +import { PremiumV2Component } from "./premium/premium-v2.component"; +import { PremiumComponent } from "./premium/premium.component"; import { SubscriptionComponent } from "./subscription.component"; import { UserSubscriptionComponent } from "./user-subscription.component"; @@ -20,11 +24,15 @@ const routes: Routes = [ component: UserSubscriptionComponent, data: { titleId: "premiumMembership" }, }, - { - path: "premium", - component: PremiumComponent, - data: { titleId: "goPremium" }, - }, + ...featureFlaggedRoute({ + defaultComponent: PremiumComponent, + flaggedComponent: PremiumV2Component, + featureFlag: FeatureFlag.AC2476_DeprecateStripeSourcesAPI, + routeOptions: { + path: "premium", + data: { titleId: "goPremium" }, + }, + }), { path: "payment-method", component: PaymentMethodComponent, diff --git a/apps/web/src/app/billing/individual/individual-billing.module.ts b/apps/web/src/app/billing/individual/individual-billing.module.ts index dbae28858f8..0dbbc8c6837 100644 --- a/apps/web/src/app/billing/individual/individual-billing.module.ts +++ b/apps/web/src/app/billing/individual/individual-billing.module.ts @@ -5,7 +5,8 @@ import { BillingSharedModule } from "../shared"; import { BillingHistoryViewComponent } from "./billing-history-view.component"; import { IndividualBillingRoutingModule } from "./individual-billing-routing.module"; -import { PremiumComponent } from "./premium.component"; +import { PremiumV2Component } from "./premium/premium-v2.component"; +import { PremiumComponent } from "./premium/premium.component"; import { SubscriptionComponent } from "./subscription.component"; import { UserSubscriptionComponent } from "./user-subscription.component"; @@ -16,6 +17,7 @@ import { UserSubscriptionComponent } from "./user-subscription.component"; BillingHistoryViewComponent, UserSubscriptionComponent, PremiumComponent, + PremiumV2Component, ], }) export class IndividualBillingModule {} diff --git a/apps/web/src/app/billing/individual/premium/premium-v2.component.html b/apps/web/src/app/billing/individual/premium/premium-v2.component.html new file mode 100644 index 00000000000..bdf6ff87d19 --- /dev/null +++ b/apps/web/src/app/billing/individual/premium/premium-v2.component.html @@ -0,0 +1,144 @@ + +

{{ "goPremium" | i18n }}

+ + {{ "alreadyPremiumFromOrg" | i18n }} + + +

{{ "premiumUpgradeUnlockFeatures" | i18n }}

+ +

+ {{ + "premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount + }} + + {{ "bitwardenFamiliesPlan" | i18n }} + +

+ + {{ "purchasePremium" | i18n }} + +
+
+ +

{{ "uploadLicenseFilePremium" | i18n }}

+
+ + {{ "licenseFile" | i18n }} +
+ + {{ + licenseFormGroup.value.file ? licenseFormGroup.value.file.name : ("noFileChosen" | i18n) + }} +
+ + {{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }} +
+ +
+
+
+ +

{{ "addons" | i18n }}

+
+ + {{ "additionalStorageGb" | i18n }} + + {{ + "additionalStorageIntervalDesc" + | i18n: "1 GB" : (storageGBPrice | currency: "$") : ("year" | i18n) + }} + +
+
+ +

{{ "summary" | i18n }}

+ {{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }}
+ {{ "additionalStorageGb" | i18n }}: {{ addOnFormGroup.value.additionalStorage || 0 }} GB × + {{ storageGBPrice | currency: "$" }} = + {{ additionalStorageCost | currency: "$" }} +
+
+ +

{{ "paymentInformation" | i18n }}

+ + +
+
+ {{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }} + + {{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }} +
+
+
+

+ {{ "total" | i18n }}: {{ total | currency: "USD $" }}/{{ "year" | i18n }} +

+ +
+
diff --git a/apps/web/src/app/billing/individual/premium/premium-v2.component.ts b/apps/web/src/app/billing/individual/premium/premium-v2.component.ts new file mode 100644 index 00000000000..cf66dac2f76 --- /dev/null +++ b/apps/web/src/app/billing/individual/premium/premium-v2.component.ts @@ -0,0 +1,164 @@ +import { Component, ViewChild } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; +import { ActivatedRoute, Router } from "@angular/router"; +import { combineLatest, concatMap, from, Observable, of } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; +import { ToastService } from "@bitwarden/components"; + +import { PaymentV2Component } from "../../shared/payment/payment-v2.component"; +import { TaxInfoComponent } from "../../shared/tax-info.component"; + +@Component({ + templateUrl: "./premium-v2.component.html", +}) +export class PremiumV2Component { + @ViewChild(PaymentV2Component) paymentComponent: PaymentV2Component; + @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; + + protected hasPremiumFromAnyOrganization$: Observable; + + protected addOnFormGroup = new FormGroup({ + additionalStorage: new FormControl(0, [Validators.min(0), Validators.max(99)]), + }); + + protected licenseFormGroup = new FormGroup({ + file: new FormControl(null, [Validators.required]), + }); + + protected cloudWebVaultURL: string; + protected isSelfHost = false; + + protected readonly familyPlanMaxUserCount = 6; + protected readonly premiumPrice = 10; + protected readonly storageGBPrice = 4; + + constructor( + private activatedRoute: ActivatedRoute, + private apiService: ApiService, + private billingAccountProfileStateService: BillingAccountProfileStateService, + private environmentService: EnvironmentService, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private router: Router, + private syncService: SyncService, + private toastService: ToastService, + private tokenService: TokenService, + ) { + this.isSelfHost = this.platformUtilsService.isSelfHost(); + + this.hasPremiumFromAnyOrganization$ = + this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$; + + combineLatest([ + this.billingAccountProfileStateService.hasPremiumPersonally$, + this.environmentService.cloudWebVaultUrl$, + ]) + .pipe( + takeUntilDestroyed(), + concatMap(([hasPremiumPersonally, cloudWebVaultURL]) => { + if (hasPremiumPersonally) { + return from(this.navigateToSubscriptionPage()); + } + + this.cloudWebVaultURL = cloudWebVaultURL; + return of(true); + }), + ) + .subscribe(); + } + + finalizeUpgrade = async () => { + await this.apiService.refreshIdentityToken(); + await this.syncService.fullSync(true); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("premiumUpdated"), + }); + await this.navigateToSubscriptionPage(); + }; + + navigateToSubscriptionPage = (): Promise => + this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute }); + + onLicenseFileSelected = (event: Event): void => { + const element = event.target as HTMLInputElement; + this.licenseFormGroup.value.file = element.files.length > 0 ? element.files[0] : null; + }; + + submitPremiumLicense = async (): Promise => { + this.licenseFormGroup.markAllAsTouched(); + + if (this.licenseFormGroup.invalid) { + return this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("selectFile"), + }); + } + + const emailVerified = await this.tokenService.getEmailVerified(); + if (!emailVerified) { + return this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("verifyEmailFirst"), + }); + } + + const formData = new FormData(); + formData.append("license", this.licenseFormGroup.value.file); + + await this.apiService.postAccountLicense(formData); + await this.finalizeUpgrade(); + }; + + submitPayment = async (): Promise => { + this.taxInfoComponent.taxFormGroup.markAllAsTouched(); + if (this.taxInfoComponent.taxFormGroup.invalid) { + return; + } + + const { type, token } = await this.paymentComponent.tokenize(); + + const formData = new FormData(); + formData.append("paymentMethodType", type.toString()); + formData.append("paymentToken", token); + formData.append("additionalStorageGb", this.addOnFormGroup.value.additionalStorage.toString()); + formData.append("country", this.taxInfoComponent.country); + formData.append("postalCode", this.taxInfoComponent.postalCode); + + await this.apiService.postPremium(formData); + await this.finalizeUpgrade(); + }; + + protected get additionalStorageCost(): number { + return this.storageGBPrice * this.addOnFormGroup.value.additionalStorage; + } + + protected get estimatedTax(): number { + return this.taxInfoComponent?.taxRate != null + ? (this.taxInfoComponent.taxRate / 100) * this.subtotal + : 0; + } + + protected get premiumURL(): string { + return `${this.cloudWebVaultURL}/#/settings/subscription/premium`; + } + + protected get subtotal(): number { + return this.premiumPrice + this.additionalStorageCost; + } + + protected get total(): number { + return this.subtotal + this.estimatedTax; + } +} diff --git a/apps/web/src/app/billing/individual/premium.component.html b/apps/web/src/app/billing/individual/premium/premium.component.html similarity index 100% rename from apps/web/src/app/billing/individual/premium.component.html rename to apps/web/src/app/billing/individual/premium/premium.component.html diff --git a/apps/web/src/app/billing/individual/premium.component.ts b/apps/web/src/app/billing/individual/premium/premium.component.ts similarity index 98% rename from apps/web/src/app/billing/individual/premium.component.ts rename to apps/web/src/app/billing/individual/premium/premium.component.ts index 79a5c5e2edd..c45b6b882d4 100644 --- a/apps/web/src/app/billing/individual/premium.component.ts +++ b/apps/web/src/app/billing/individual/premium/premium.component.ts @@ -13,7 +13,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { ToastService } from "@bitwarden/components"; -import { PaymentComponent, TaxInfoComponent } from "../shared"; +import { PaymentComponent, TaxInfoComponent } from "../../shared"; @Component({ templateUrl: "premium.component.html", 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 c9b3f2de855..b966729c1df 100644 --- a/apps/web/src/app/billing/shared/billing-shared.module.ts +++ b/apps/web/src/app/billing/shared/billing-shared.module.ts @@ -52,6 +52,7 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac UpdateLicenseDialogComponent, OffboardingSurveyComponent, VerifyBankAccountComponent, + PaymentV2Component, ], }) export class BillingSharedModule {} From 74d510332d508d6c5242bbf9e8b8abcb6d4e6ecb Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Thu, 5 Sep 2024 17:46:03 +0200 Subject: [PATCH 02/12] [PM-9884] Create empty send add/edit page for browser (#10155) * Move SendV2component into send-v2 subFolder * Create SendFormContainer and related services * Add initial SendFormComponent which uses the SendFormContainer * Remove AdditionalOptionsSectionComponent which will be added with a future PR * Add libs/tools/send to root tsconfig * Register libs/tools/send/send-ui with root jest.config.js * Register libs/tools/send/send-ui with root tailwind.config.js * Create empty Send add edit page - Introduces conditional routing based on extension refresh feature flag - After selecting a Send type via the New button navigate to the new send-add-edit page and build a SendFormConfig * Fix service injection on DefaultSendFormService * Rename setHeader into getHeaderText, make it private and add documentation * Set radix/base to 10 for parseInt * Add documentation * Rename local variable * Removed unneeded loading state * Remove unused originalSendId * Run prettier * Add link to edit an existing send --------- Co-authored-by: Daniel James Smith --- apps/browser/src/popup/app-routing.module.ts | 11 +- .../add-edit/send-add-edit.component.html | 17 +++ .../add-edit/send-add-edit.component.ts | 141 ++++++++++++++++++ .../send-list-items-container.component.html | 2 + 4 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html create mode 100644 apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index aa8955035dd..7af88316842 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -59,6 +59,7 @@ import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/pass import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component"; import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component"; import { SendTypeComponent } from "../tools/popup/send/send-type.component"; +import { SendAddEditComponent as SendAddEditV2Component } from "../tools/popup/send-v2/add-edit/send-add-edit.component"; import { SendCreatedComponent } from "../tools/popup/send-v2/send-created/send-created.component"; import { SendV2Component } from "../tools/popup/send-v2/send-v2.component"; import { AboutPageV2Component } from "../tools/popup/settings/about-page/about-page-v2.component"; @@ -362,18 +363,16 @@ const routes: Routes = [ canActivate: [authGuard], data: { state: "send-type" }, }, - { + ...extensionRefreshSwap(SendAddEditComponent, SendAddEditV2Component, { path: "add-send", - component: SendAddEditComponent, canActivate: [authGuard], data: { state: "add-send" }, - }, - { + }), + ...extensionRefreshSwap(SendAddEditComponent, SendAddEditV2Component, { path: "edit-send", - component: SendAddEditComponent, canActivate: [authGuard], data: { state: "edit-send" }, - }, + }), { path: "send-created", component: SendCreatedComponent, diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html new file mode 100644 index 00000000000..3e9a8d7c50d --- /dev/null +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts new file mode 100644 index 00000000000..48e6cbb8a31 --- /dev/null +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts @@ -0,0 +1,141 @@ +import { CommonModule, Location } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormsModule } from "@angular/forms"; +import { ActivatedRoute, Params } from "@angular/router"; +import { map, switchMap } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { SendId } from "@bitwarden/common/types/guid"; +import { AsyncActionsModule, ButtonModule, SearchModule } from "@bitwarden/components"; +import { + DefaultSendFormConfigService, + SendFormConfig, + SendFormConfigService, + SendFormMode, +} from "@bitwarden/send-ui"; + +import { SendFormModule } from "../../../../../../../libs/tools/send/send-ui/src/send-form/send-form.module"; +import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component"; +import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component"; + +/** + * Helper class to parse query parameters for the AddEdit route. + */ +class QueryParams { + constructor(params: Params) { + this.sendId = params.sendId; + this.type = parseInt(params.type, 10); + } + + /** + * The ID of the send to edit, empty when it's a new Send + */ + sendId?: SendId; + + /** + * The type of send to create. + */ + type: SendType; +} + +export type AddEditQueryParams = Partial>; + +/** + * Component for adding or editing a send item. + */ +@Component({ + selector: "tools-send-add-edit", + templateUrl: "send-add-edit.component.html", + standalone: true, + providers: [{ provide: SendFormConfigService, useClass: DefaultSendFormConfigService }], + imports: [ + CommonModule, + SearchModule, + JslibModule, + FormsModule, + ButtonModule, + PopupPageComponent, + PopupHeaderComponent, + PopupFooterComponent, + SendFormModule, + AsyncActionsModule, + ], +}) +export class SendAddEditComponent { + /** + * The header text for the component. + */ + headerText: string; + + /** + * The configuration for the send form. + */ + config: SendFormConfig; + + constructor( + private route: ActivatedRoute, + private location: Location, + private i18nService: I18nService, + private addEditFormConfigService: SendFormConfigService, + ) { + this.subscribeToParams(); + } + + /** + * Handles the event when the send is saved. + */ + onSendSaved() { + this.location.back(); + } + + /** + * Subscribes to the route query parameters and builds the configuration based on the parameters. + */ + subscribeToParams(): void { + this.route.queryParams + .pipe( + takeUntilDestroyed(), + map((params) => new QueryParams(params)), + switchMap(async (params) => { + let mode: SendFormMode; + if (params.sendId == null) { + mode = "add"; + } else { + mode = "edit"; + } + const config = await this.addEditFormConfigService.buildConfig( + mode, + params.sendId, + params.type, + ); + return config; + }), + ) + .subscribe((config) => { + this.config = config; + this.headerText = this.getHeaderText(config.mode, config.sendType); + }); + } + + /** + * Gets the header text based on the mode and type. + * @param mode The mode of the send form. + * @param type The type of the send form. + * @returns The header text. + */ + private getHeaderText(mode: SendFormMode, type: SendType) { + const headerKey = + mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader"; + + switch (type) { + case SendType.Text: + return this.i18nService.t(headerKey, this.i18nService.t("sendTypeText")); + case SendType.File: + return this.i18nService.t(headerKey, this.i18nService.t("sendTypeFile")); + } + } +} diff --git a/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html index 7745e4d6f88..5502422a029 100644 --- a/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html +++ b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html @@ -10,6 +10,8 @@ diff --git a/apps/web/src/app/billing/shared/update-license.component.html b/apps/web/src/app/billing/shared/update-license.component.html index 938179469e4..ea0818389e4 100644 --- a/apps/web/src/app/billing/shared/update-license.component.html +++ b/apps/web/src/app/billing/shared/update-license.component.html @@ -1,7 +1,7 @@ {{ "licenseFile" | i18n }} -
+
From 0902ca5ae8d5306d953c7805819e8c6a67a54b57 Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:33:26 -0400 Subject: [PATCH 04/12] Bumped client version(s) (#10910) --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- package-lock.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index 433ddecd2ac..c5332a08016 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.8.1", + "version": "2024.8.2", "scripts": { "build": "cross-env MANIFEST_VERSION=3 webpack", "build:mv2": "webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 121897b0cef..5792c10c053 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.8.1", + "version": "2024.8.2", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 52e21a0936b..fe99c9e9889 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.8.1", + "version": "2024.8.2", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/package-lock.json b/package-lock.json index 73149dded8c..6218d7f25dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -193,7 +193,7 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2024.8.1" + "version": "2024.8.2" }, "apps/cli": { "name": "@bitwarden/cli", From 352ab29665d8bc4145d0504d197c8713c2b56327 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Thu, 5 Sep 2024 13:55:30 -0400 Subject: [PATCH 05/12] Auth/PM-11635 - Extension Registration Bugfix - Unable to login after account creation issue (#10896) * PM-11635 - Refactor Base Login Component NgOnInit to properly connect the loading of email settings from state to the processing of query params. If an email is not passed via query params (like registration / normal login), then we will try to load the email from state. * PM-11635 - Extension login component - call parent ngOnInit as it is responsible for processing / setting email from either query params or state. The addition of the child ngOnInit prevented the parent component ngOnInit from executing. --- .../browser/src/auth/popup/login.component.ts | 4 +- .../src/auth/components/login.component.ts | 37 ++++++++++++------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/apps/browser/src/auth/popup/login.component.ts b/apps/browser/src/auth/popup/login.component.ts index 09bfdbbc240..ea72fb61f5f 100644 --- a/apps/browser/src/auth/popup/login.component.ts +++ b/apps/browser/src/auth/popup/login.component.ts @@ -86,10 +86,8 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { } async ngOnInit(): Promise { + await super.ngOnInit(); if (this.showPasswordless) { - const loginEmail = await firstValueFrom(this.loginEmailService.loginEmail$); - this.formGroup.controls.email.setValue(loginEmail); - this.formGroup.controls.rememberEmail.setValue(this.loginEmailService.getRememberEmail()); await this.validateEmail(); } } diff --git a/libs/angular/src/auth/components/login.component.ts b/libs/angular/src/auth/components/login.component.ts index 3b927a05716..b798a8df0b4 100644 --- a/libs/angular/src/auth/components/login.component.ts +++ b/libs/angular/src/auth/components/login.component.ts @@ -1,8 +1,8 @@ import { Directive, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { Subject, firstValueFrom } from "rxjs"; -import { take, takeUntil } from "rxjs/operators"; +import { Subject, firstValueFrom, of } from "rxjs"; +import { switchMap, take, takeUntil } from "rxjs/operators"; import { LoginStrategyServiceAbstraction, @@ -99,20 +99,31 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, } async ngOnInit() { - this.route?.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => { - if (!params) { - return; - } + this.route?.queryParams + .pipe( + switchMap((params) => { + if (!params) { + // If no params,loadEmailSettings from state + return this.loadEmailSettings(); + } - const queryParamsEmail = params.email; + const queryParamsEmail = params.email; - if (queryParamsEmail != null && queryParamsEmail.indexOf("@") > -1) { - this.formGroup.controls.email.setValue(queryParamsEmail); - this.paramEmailSet = true; - } - }); + if (queryParamsEmail != null && queryParamsEmail.indexOf("@") > -1) { + this.formGroup.controls.email.setValue(queryParamsEmail); + this.paramEmailSet = true; + } - if (!this.paramEmailSet) { + // If paramEmailSet is false, loadEmailSettings from state + return this.paramEmailSet ? of(null) : this.loadEmailSettings(); + }), + takeUntil(this.destroy$), + ) + .subscribe(); + + // Backup check to handle unknown case where activatedRoute is not available + // This shouldn't happen under normal circumstances + if (!this.route) { await this.loadEmailSettings(); } } From 63e3f3218b978ef34bdf4772901ee4b2164659ef Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Thu, 5 Sep 2024 14:05:23 -0400 Subject: [PATCH 06/12] Bumped client version(s) (#10912) --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 68be97abfe1..70a0d9cb8c2 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2024.8.3", + "version": "2024.9.0", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index a37c185d170..6823bddceb8 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.8.3", + "version": "2024.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.8.3", + "version": "2024.9.0", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index df1f7f70d41..18a046d5bce 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2024.8.3", + "version": "2024.9.0", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index 6218d7f25dc..b28f5323a04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -233,7 +233,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.8.3", + "version": "2024.9.0", "hasInstallScript": true, "license": "GPL-3.0" }, From f58138a51d86e151134c7a44c17e5dfc79e072fd Mon Sep 17 00:00:00 2001 From: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com> Date: Thu, 5 Sep 2024 14:18:22 -0400 Subject: [PATCH 07/12] Bumped client version(s) (#10913) --- 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 391cd4b5cc6..de153ea1a23 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.8.2", + "version": "2024.8.3", "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 b28f5323a04..49346dbe885 100644 --- a/package-lock.json +++ b/package-lock.json @@ -247,7 +247,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.8.2" + "version": "2024.8.3" }, "libs/admin-console": { "name": "@bitwarden/admin-console", From 3bbc2cc691536a82bbf92f0a9c509c9bbdfdc0fc Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 5 Sep 2024 14:44:19 -0400 Subject: [PATCH 08/12] Change AppIdService to Use Storage Directly (#10835) --- .../browser/src/background/main.background.ts | 2 +- .../service-container/service-container.ts | 2 +- apps/web/src/app/core/core.module.ts | 7 ++ .../src/services/jslib-services.module.ts | 2 +- .../platform/abstractions/app-id.service.ts | 4 - .../platform/services/app-id.service.spec.ts | 59 +++++--------- .../src/platform/services/app-id.service.ts | 81 ++++++++----------- 7 files changed, 65 insertions(+), 92 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 124cf6a78d8..2c48a8563e7 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -652,7 +652,7 @@ export default class MainBackground { this.kdfConfigService, ); - this.appIdService = new AppIdService(this.globalStateProvider); + this.appIdService = new AppIdService(this.storageService, this.logService); this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); this.organizationService = new OrganizationService(this.stateProvider); diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index fb77e41a4b6..be1b1cc9e2b 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -413,7 +413,7 @@ export class ServiceContainer { this.kdfConfigService, ); - this.appIdService = new AppIdService(this.globalStateProvider); + this.appIdService = new AppIdService(this.storageService, this.logService); const customUserAgent = "Bitwarden_CLI/" + diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 460af8623e6..887c8fb626a 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -32,6 +32,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { ClientType } from "@bitwarden/common/enums"; +import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; @@ -41,6 +42,7 @@ import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwar import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; +import { AppIdService as DefaultAppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; // eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; @@ -207,6 +209,11 @@ const safeProviders: SafeProvider[] = [ InternalUserDecryptionOptionsServiceAbstraction, ], }), + safeProvider({ + provide: AppIdService, + useClass: DefaultAppIdService, + deps: [OBSERVABLE_DISK_LOCAL_STORAGE, LogService], + }), ]; @NgModule({ diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 7b9c33f3d0a..ab52ab8e433 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -370,7 +370,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: AppIdServiceAbstraction, useClass: AppIdService, - deps: [GlobalStateProvider], + deps: [OBSERVABLE_DISK_STORAGE, LogService], }), safeProvider({ provide: AuditServiceAbstraction, diff --git a/libs/common/src/platform/abstractions/app-id.service.ts b/libs/common/src/platform/abstractions/app-id.service.ts index c2c1a23ef5e..7fb02ee9e09 100644 --- a/libs/common/src/platform/abstractions/app-id.service.ts +++ b/libs/common/src/platform/abstractions/app-id.service.ts @@ -1,8 +1,4 @@ -import { Observable } from "rxjs"; - export abstract class AppIdService { - abstract appId$: Observable; - abstract anonymousAppId$: Observable; abstract getAppId(): Promise; abstract getAnonymousAppId(): Promise; } diff --git a/libs/common/src/platform/services/app-id.service.spec.ts b/libs/common/src/platform/services/app-id.service.spec.ts index 62806204db0..23691769bf2 100644 --- a/libs/common/src/platform/services/app-id.service.spec.ts +++ b/libs/common/src/platform/services/app-id.service.spec.ts @@ -1,19 +1,18 @@ -import { FakeGlobalState, FakeGlobalStateProvider, ObservableTracker } from "../../../spec"; +import { mock } from "jest-mock-extended"; + +import { FakeStorageService } from "../../../spec"; +import { LogService } from "../abstractions/log.service"; import { Utils } from "../misc/utils"; import { ANONYMOUS_APP_ID_KEY, APP_ID_KEY, AppIdService } from "./app-id.service"; describe("AppIdService", () => { - let globalStateProvider: FakeGlobalStateProvider; - let appIdState: FakeGlobalState; - let anonymousAppIdState: FakeGlobalState; + let fakeStorageService: FakeStorageService; let sut: AppIdService; beforeEach(() => { - globalStateProvider = new FakeGlobalStateProvider(); - appIdState = globalStateProvider.getFake(APP_ID_KEY); - anonymousAppIdState = globalStateProvider.getFake(ANONYMOUS_APP_ID_KEY); - sut = new AppIdService(globalStateProvider); + fakeStorageService = new FakeStorageService(); + sut = new AppIdService(fakeStorageService, mock()); }); afterEach(() => { @@ -22,7 +21,7 @@ describe("AppIdService", () => { describe("getAppId", () => { it("returns the existing appId when it exists", async () => { - appIdState.stateSubject.next("existingAppId"); + fakeStorageService.internalUpdateStore({ [APP_ID_KEY]: "existingAppId" }); const appId = await sut.getAppId(); @@ -30,7 +29,7 @@ describe("AppIdService", () => { }); it("creates a new appId only once", async () => { - appIdState.stateSubject.next(null); + fakeStorageService.internalUpdateStore({ [APP_ID_KEY]: null }); const appIds: string[] = []; const promises = [async () => appIds.push(await sut.getAppId())]; @@ -41,7 +40,7 @@ describe("AppIdService", () => { }); it.each([null, undefined])("returns a new appId when %s", async (value) => { - appIdState.stateSubject.next(value); + fakeStorageService.internalUpdateStore({ [APP_ID_KEY]: value }); const appId = await sut.getAppId(); @@ -49,27 +48,17 @@ describe("AppIdService", () => { }); it.each([null, undefined])("stores the new guid when %s", async (value) => { - appIdState.stateSubject.next(value); + fakeStorageService.internalUpdateStore({ [APP_ID_KEY]: value }); const appId = await sut.getAppId(); - expect(appIdState.nextMock).toHaveBeenCalledWith(appId); - }); - - it("emits only once when creating a new appId", async () => { - appIdState.stateSubject.next(null); - - const tracker = new ObservableTracker(sut.appId$); - const appId = await sut.getAppId(); - - expect(tracker.emissions).toEqual([appId]); - await expect(tracker.pauseUntilReceived(2, 50)).rejects.toThrow("Timeout exceeded"); + expect(fakeStorageService.mock.save).toHaveBeenCalledWith(APP_ID_KEY, appId, undefined); }); }); describe("getAnonymousAppId", () => { it("returns the existing appId when it exists", async () => { - anonymousAppIdState.stateSubject.next("existingAppId"); + fakeStorageService.internalUpdateStore({ [ANONYMOUS_APP_ID_KEY]: "existingAppId" }); const appId = await sut.getAnonymousAppId(); @@ -77,7 +66,7 @@ describe("AppIdService", () => { }); it("creates a new anonymousAppId only once", async () => { - anonymousAppIdState.stateSubject.next(null); + fakeStorageService.internalUpdateStore({ [ANONYMOUS_APP_ID_KEY]: null }); const appIds: string[] = []; const promises = [async () => appIds.push(await sut.getAnonymousAppId())]; @@ -88,7 +77,7 @@ describe("AppIdService", () => { }); it.each([null, undefined])("returns a new appId when it does not exist", async (value) => { - anonymousAppIdState.stateSubject.next(value); + fakeStorageService.internalUpdateStore({ [ANONYMOUS_APP_ID_KEY]: value }); const appId = await sut.getAnonymousAppId(); @@ -98,22 +87,16 @@ describe("AppIdService", () => { it.each([null, undefined])( "stores the new guid when it an existing one is not found", async (value) => { - anonymousAppIdState.stateSubject.next(value); + fakeStorageService.internalUpdateStore({ [ANONYMOUS_APP_ID_KEY]: value }); const appId = await sut.getAnonymousAppId(); - expect(anonymousAppIdState.nextMock).toHaveBeenCalledWith(appId); + expect(fakeStorageService.mock.save).toHaveBeenCalledWith( + ANONYMOUS_APP_ID_KEY, + appId, + undefined, + ); }, ); - - it("emits only once when creating a new anonymousAppId", async () => { - anonymousAppIdState.stateSubject.next(null); - - const tracker = new ObservableTracker(sut.anonymousAppId$); - const appId = await sut.getAnonymousAppId(); - - expect(tracker.emissions).toEqual([appId]); - await expect(tracker.pauseUntilReceived(2, 50)).rejects.toThrow("Timeout exceeded"); - }); }); }); diff --git a/libs/common/src/platform/services/app-id.service.ts b/libs/common/src/platform/services/app-id.service.ts index 3578d96c409..97d7876743b 100644 --- a/libs/common/src/platform/services/app-id.service.ts +++ b/libs/common/src/platform/services/app-id.service.ts @@ -1,59 +1,46 @@ -import { Observable, concatMap, distinctUntilChanged, firstValueFrom, share } from "rxjs"; - import { AppIdService as AppIdServiceAbstraction } from "../abstractions/app-id.service"; +import { LogService } from "../abstractions/log.service"; +import { AbstractStorageService } from "../abstractions/storage.service"; import { Utils } from "../misc/utils"; -import { APPLICATION_ID_DISK, GlobalStateProvider, KeyDefinition } from "../state"; -export const APP_ID_KEY = new KeyDefinition(APPLICATION_ID_DISK, "appId", { - deserializer: (value: string) => value, - cleanupDelayMs: 0, - debug: { - enableRetrievalLogging: true, - enableUpdateLogging: true, - }, -}); -export const ANONYMOUS_APP_ID_KEY = new KeyDefinition(APPLICATION_ID_DISK, "anonymousAppId", { - deserializer: (value: string) => value, -}); +// export const APP_ID_KEY = new KeyDefinition(APPLICATION_ID_DISK, "appId", { +// deserializer: (value: string) => value, +// cleanupDelayMs: 0, +// debug: { +// enableRetrievalLogging: true, +// enableUpdateLogging: true, +// }, +// }); +// export const ANONYMOUS_APP_ID_KEY = new KeyDefinition(APPLICATION_ID_DISK, "anonymousAppId", { +// deserializer: (value: string) => value, +// }); + +export const APP_ID_KEY = "global_applicationId_appId"; +export const ANONYMOUS_APP_ID_KEY = "global_applicationId_appId"; export class AppIdService implements AppIdServiceAbstraction { - appId$: Observable; - anonymousAppId$: Observable; - - constructor(globalStateProvider: GlobalStateProvider) { - const appIdState = globalStateProvider.get(APP_ID_KEY); - const anonymousAppIdState = globalStateProvider.get(ANONYMOUS_APP_ID_KEY); - this.appId$ = appIdState.state$.pipe( - concatMap(async (appId) => { - if (!appId) { - return await appIdState.update(() => Utils.newGuid(), { - shouldUpdate: (v) => v == null, - }); - } - return appId; - }), - distinctUntilChanged(), - share(), - ); - this.anonymousAppId$ = anonymousAppIdState.state$.pipe( - concatMap(async (appId) => { - if (!appId) { - return await anonymousAppIdState.update(() => Utils.newGuid(), { - shouldUpdate: (v) => v == null, - }); - } - return appId; - }), - distinctUntilChanged(), - share(), - ); - } + constructor( + private readonly storageService: AbstractStorageService, + private readonly logService: LogService, + ) {} async getAppId(): Promise { - return await firstValueFrom(this.appId$); + this.logService.info("Retrieving application id"); + return await this.getEnsuredValue(APP_ID_KEY); } async getAnonymousAppId(): Promise { - return await firstValueFrom(this.anonymousAppId$); + return await this.getEnsuredValue(ANONYMOUS_APP_ID_KEY); + } + + private async getEnsuredValue(key: string) { + let value = await this.storageService.get(key); + + if (value == null) { + value = Utils.newGuid(); + await this.storageService.save(key, value); + } + + return value; } } From ef3b3c1cb56cfc483e2116ee54ece414c7605e57 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:36:43 -0400 Subject: [PATCH 09/12] Remove Comments (#10914) --- libs/common/src/platform/services/app-id.service.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/libs/common/src/platform/services/app-id.service.ts b/libs/common/src/platform/services/app-id.service.ts index 97d7876743b..7b42b71bea5 100644 --- a/libs/common/src/platform/services/app-id.service.ts +++ b/libs/common/src/platform/services/app-id.service.ts @@ -3,18 +3,6 @@ import { LogService } from "../abstractions/log.service"; import { AbstractStorageService } from "../abstractions/storage.service"; import { Utils } from "../misc/utils"; -// export const APP_ID_KEY = new KeyDefinition(APPLICATION_ID_DISK, "appId", { -// deserializer: (value: string) => value, -// cleanupDelayMs: 0, -// debug: { -// enableRetrievalLogging: true, -// enableUpdateLogging: true, -// }, -// }); -// export const ANONYMOUS_APP_ID_KEY = new KeyDefinition(APPLICATION_ID_DISK, "anonymousAppId", { -// deserializer: (value: string) => value, -// }); - export const APP_ID_KEY = "global_applicationId_appId"; export const ANONYMOUS_APP_ID_KEY = "global_applicationId_appId"; From 397141b9d950cf34440e21761fdf932dbbaefa6e Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 5 Sep 2024 20:40:27 +0100 Subject: [PATCH 10/12] Resolve the issue of blank seat and seat limit pm (#10902) --- .../adjust-subscription.component.ts | 48 ++++++++++--------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts index 436372c049b..4fb9ae386a1 100644 --- a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts @@ -1,6 +1,6 @@ -import { Component, EventEmitter, Input, Output } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; +import { Subject, takeUntil } from "rxjs"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-subscription-update.request"; @@ -11,7 +11,7 @@ import { ToastService } from "@bitwarden/components"; selector: "app-adjust-subscription", templateUrl: "adjust-subscription.component.html", }) -export class AdjustSubscription { +export class AdjustSubscription implements OnInit, OnDestroy { @Input() organizationId: string; @Input() maxAutoscaleSeats: number; @Input() currentSeatCount: number; @@ -19,6 +19,8 @@ export class AdjustSubscription { @Input() interval = "year"; @Output() onAdjusted = new EventEmitter(); + private destroy$ = new Subject(); + adjustSubscriptionForm = this.formBuilder.group({ newSeatCount: [0, [Validators.min(0)]], limitSubscription: [false], @@ -30,30 +32,25 @@ export class AdjustSubscription { private organizationApiService: OrganizationApiServiceAbstraction, private formBuilder: FormBuilder, private toastService: ToastService, - ) { + ) {} + + ngOnInit() { + this.adjustSubscriptionForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => { + const maxAutoscaleSeatsControl = this.adjustSubscriptionForm.controls.newMaxSeats; + + if (value.limitSubscription) { + maxAutoscaleSeatsControl.setValidators([Validators.min(value.newSeatCount)]); + maxAutoscaleSeatsControl.enable({ emitEvent: false }); + } else { + maxAutoscaleSeatsControl.disable({ emitEvent: false }); + } + }); + this.adjustSubscriptionForm.patchValue({ newSeatCount: this.currentSeatCount, - limitSubscription: this.maxAutoscaleSeats != null, newMaxSeats: this.maxAutoscaleSeats, + limitSubscription: this.maxAutoscaleSeats != null, }); - this.adjustSubscriptionForm - .get("limitSubscription") - .valueChanges.pipe(takeUntilDestroyed()) - .subscribe((value: boolean) => { - if (value) { - this.adjustSubscriptionForm - .get("newMaxSeats") - .addValidators([ - Validators.min( - this.adjustSubscriptionForm.value.newSeatCount == null - ? 1 - : this.adjustSubscriptionForm.value.newSeatCount, - ), - Validators.required, - ]); - } - this.adjustSubscriptionForm.get("newMaxSeats").updateValueAndValidity(); - }); } submit = async () => { @@ -99,4 +96,9 @@ export class AdjustSubscription { get limitSubscription(): boolean { return this.adjustSubscriptionForm.value.limitSubscription; } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } } From 7dcfaf8271f2dc84d27a86477df91b9c25e166bf Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Thu, 5 Sep 2024 14:41:39 -0500 Subject: [PATCH 11/12] add title for attachment name (#10908) - adds the ability for a user to see the attachment name while hovering over it --- .../components/attachments/cipher-attachments.component.html | 2 +- .../cipher-view/attachments/attachments-v2-view.component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.html b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.html index 159cab31e86..f7414bd8d3c 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.html +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.html @@ -4,7 +4,7 @@
  • - {{ attachment.fileName }} + {{ attachment.fileName }} {{ attachment.sizeName }} diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html index 467dd690c64..59d97f07f20 100644 --- a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html +++ b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.html @@ -5,7 +5,7 @@ - {{ attachment.fileName }} + {{ attachment.fileName }} {{ attachment.sizeName }} From 3f25b9bbfc7cdd9f6cbdd682909534ff2dc3f19f Mon Sep 17 00:00:00 2001 From: Victoria League Date: Thu, 5 Sep 2024 16:38:20 -0400 Subject: [PATCH 12/12] [CL-357] Remove border radius when displayed in web app (#10884) --- libs/components/src/card/card.component.ts | 2 +- libs/components/src/card/card.stories.ts | 33 ++++++++++++- libs/components/src/item/item.component.html | 2 +- libs/components/src/item/item.mdx | 36 +++++++------- libs/components/src/item/item.stories.ts | 49 ++++++++++++++++++++ 5 files changed, 100 insertions(+), 22 deletions(-) diff --git a/libs/components/src/card/card.component.ts b/libs/components/src/card/card.component.ts index da61d536642..3aaed26d8d0 100644 --- a/libs/components/src/card/card.component.ts +++ b/libs/components/src/card/card.component.ts @@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, Component } from "@angular/core"; changeDetection: ChangeDetectionStrategy.OnPush, host: { class: - "tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 tw-rounded-lg tw-py-4 tw-px-3", + "tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 [&:not(bit-layout_*)]:tw-rounded-lg tw-py-4 tw-px-3", }, }) export class CardComponent {} diff --git a/libs/components/src/card/card.stories.ts b/libs/components/src/card/card.stories.ts index 702a8aeb631..b33f5f4a198 100644 --- a/libs/components/src/card/card.stories.ts +++ b/libs/components/src/card/card.stories.ts @@ -1,7 +1,12 @@ +import { RouterTestingModule } from "@angular/router/testing"; import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { LayoutComponent } from "../layout"; import { SectionComponent } from "../section"; import { TypographyModule } from "../typography"; +import { I18nMockService } from "../utils/i18n-mock.service"; import { CardComponent } from "./card.component"; @@ -10,7 +15,20 @@ export default { component: CardComponent, decorators: [ moduleMetadata({ - imports: [TypographyModule, SectionComponent], + imports: [TypographyModule, SectionComponent, LayoutComponent, RouterTestingModule], + providers: [ + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + toggleSideNavigation: "Toggle side navigation", + skipToContent: "Skip to content", + submenu: "submenu", + toggleCollapse: "toggle collapse", + }); + }, + }, + ], }), componentWrapperDecorator( (story) => `
    ${story}
    `, @@ -60,3 +78,16 @@ export const WithinSections: Story = { `, }), }; + +export const WithoutBorderRadius: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + +

    Cards used in bit-layout will not have a border radius

    +
    +
    + `, + }), +}; diff --git a/libs/components/src/item/item.component.html b/libs/components/src/item/item.component.html index c02117058f1..55e69006c57 100644 --- a/libs/components/src/item/item.component.html +++ b/libs/components/src/item/item.component.html @@ -1,6 +1,6 @@
    - - + + +
    + +Items used within a parent `bit-layout` component will not have a border radius, since the +`bit-layout` background is white. + + + +
    +
    ## Primary Content @@ -41,9 +49,7 @@ The content can be a button, anchor, or static container. ``` - - - + ### Content Slots @@ -74,9 +80,7 @@ The content can be a button, anchor, or static container. ``` - - - + ## Secondary Actions @@ -109,13 +113,9 @@ Actions are commonly icon buttons or badge buttons. Groups of items can be associated by wrapping them in the ``. - - - + - - - + ### A11y @@ -136,6 +136,4 @@ Use `aria-label` or `aria-labelledby` to give groups an accessible name. ### Virtual Scrolling - - - + diff --git a/libs/components/src/item/item.stories.ts b/libs/components/src/item/item.stories.ts index e8accfd6933..b5212fd871a 100644 --- a/libs/components/src/item/item.stories.ts +++ b/libs/components/src/item/item.stories.ts @@ -1,12 +1,17 @@ import { ScrollingModule } from "@angular/cdk/scrolling"; import { CommonModule } from "@angular/common"; +import { RouterTestingModule } from "@angular/router/testing"; import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + import { A11yGridDirective } from "../a11y/a11y-grid.directive"; import { AvatarModule } from "../avatar"; import { BadgeModule } from "../badge"; import { IconButtonModule } from "../icon-button"; +import { LayoutComponent } from "../layout"; import { TypographyModule } from "../typography"; +import { I18nMockService } from "../utils/i18n-mock.service"; import { ItemActionComponent } from "./item-action.component"; import { ItemContentComponent } from "./item-content.component"; @@ -29,6 +34,21 @@ export default { ItemContentComponent, A11yGridDirective, ScrollingModule, + LayoutComponent, + RouterTestingModule, + ], + providers: [ + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + toggleSideNavigation: "Toggle side navigation", + skipToContent: "Skip to content", + submenu: "submenu", + toggleCollapse: "toggle collapse", + }); + }, + }, ], }), componentWrapperDecorator((story) => `
    ${story}
    `), @@ -333,3 +353,32 @@ export const VirtualScrolling: Story = { `, }), }; + +export const WithoutBorderRadius: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + + + + + + + + + + + + + + + `, + }), +};