diff --git a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts index 8cf7ed313f..c95ff754c4 100644 --- a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts @@ -1,6 +1,6 @@ import { Component } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, takeUntil, map, lastValueFrom } from "rxjs"; +import { concatMap, takeUntil, map } from "rxjs"; import { tap } from "rxjs/operators"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -16,7 +16,6 @@ import { DialogService } from "@bitwarden/components"; import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component"; import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component"; -import { TwoFactorVerifyComponent } from "../../../auth/settings/two-factor-verify.component"; @Component({ selector: "app-two-factor-setup", @@ -66,17 +65,17 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent { async manage(type: TwoFactorProviderType) { switch (type) { case TwoFactorProviderType.OrganizationDuo: { - const twoFactorVerifyDialogRef = TwoFactorVerifyComponent.open(this.dialogService, { - data: { type: type, organizationId: this.organizationId }, - }); - const result: AuthResponse = await lastValueFrom( - twoFactorVerifyDialogRef.closed, + const result: AuthResponse = await this.callTwoFactorVerifyDialog( + TwoFactorProviderType.OrganizationDuo, ); + if (!result) { return; } const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent); + duoComp.type = TwoFactorProviderType.OrganizationDuo; + duoComp.organizationId = this.organizationId; duoComp.auth(result); duoComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => { this.updateStatus(enabled, TwoFactorProviderType.OrganizationDuo); diff --git a/apps/web/src/app/billing/individual/premium.component.html b/apps/web/src/app/billing/individual/premium.component.html index f962f7cfe1..e3afa7779b 100644 --- a/apps/web/src/app/billing/individual/premium.component.html +++ b/apps/web/src/app/billing/individual/premium.component.html @@ -1,129 +1,143 @@ - -
-

{{ "goPremium" | i18n }}

-
- - {{ "alreadyPremiumFromOrg" | i18n }} - - -

{{ "premiumUpgradeUnlockFeatures" | i18n }}

-
    -
  • - - {{ "premiumSignUpStorage" | i18n }} -
  • -
  • - - {{ "premiumSignUpTwoStepOptions" | i18n }} -
  • -
  • - - {{ "premiumSignUpEmergency" | i18n }} -
  • -
  • - - {{ "premiumSignUpReports" | i18n }} -
  • -
  • - - {{ "premiumSignUpTotp" | i18n }} -
  • -
  • - - {{ "premiumSignUpSupport" | i18n }} -
  • -
  • - - {{ "premiumSignUpFuture" | i18n }} -
  • -
-

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

- +

{{ "goPremium" | i18n }}

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

{{ "uploadLicenseFilePremium" | i18n }}

-
-
- - - {{ - "licenseFileDesc" | i18n: "bitwarden_premium_license.json" - }} -
- + {{ this.licenseFile ? this.licenseFile.name : ("noFileChosen" | i18n) }} + + + {{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }} + +
-
-
-

{{ "addons" | i18n }}

-
-
- - - {{ - "additionalStorageIntervalDesc" - | i18n: "1 GB" : (storageGbPrice | currency: "$") : ("year" | i18n) - }} + + + +

{{ "addons" | i18n }}

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

{{ "summary" | i18n }}

- {{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }}
- {{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} GB × - {{ storageGbPrice | currency: "$" }} = - {{ additionalStorageTotal | currency: "$" }} -
-

{{ "paymentInformation" | i18n }}

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

{{ "summary" | i18n }}

+ {{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }}
+ {{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} GB × + {{ storageGbPrice | currency: "$" }} = + {{ additionalStorageTotal | currency: "$" }} +
+
+ +

{{ "paymentInformation" | i18n }}

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

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

-
-

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

-
- {{ "paymentChargedAnnually" | i18n }} - +

{{ "paymentChargedAnnually" | i18n }}

+ + diff --git a/apps/web/src/app/billing/individual/premium.component.ts b/apps/web/src/app/billing/individual/premium.component.ts index 60536c17b5..79a8bad75a 100644 --- a/apps/web/src/app/billing/individual/premium.component.ts +++ b/apps/web/src/app/billing/individual/premium.component.ts @@ -1,4 +1,5 @@ import { Component, OnInit, ViewChild } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; import { Router } from "@angular/router"; import { firstValueFrom, Observable } from "rxjs"; @@ -7,7 +8,6 @@ import { TokenService } from "@bitwarden/common/auth/abstractions/token.service" import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -26,11 +26,16 @@ export class PremiumComponent implements OnInit { premiumPrice = 10; familyPlanMaxUserCount = 6; storageGbPrice = 4; - additionalStorage = 0; cloudWebVaultUrl: string; + licenseFile: File = null; formPromise: Promise; - + protected licenseForm = new FormGroup({ + file: new FormControl(null, [Validators.required]), + }); + protected addonForm = new FormGroup({ + additionalStorage: new FormControl(0, [Validators.max(99), Validators.min(0)]), + }); constructor( private apiService: ApiService, private i18nService: I18nService, @@ -39,14 +44,17 @@ export class PremiumComponent implements OnInit { private router: Router, private messagingService: MessagingService, private syncService: SyncService, - private logService: LogService, private environmentService: EnvironmentService, private billingAccountProfileStateService: BillingAccountProfileStateService, ) { this.selfHosted = platformUtilsService.isSelfHost(); this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; } - + protected setSelectedFile(event: Event) { + const fileInputEl = event.target; + const file: File = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null; + this.licenseFile = file; + } async ngOnInit() { this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$); if (await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$)) { @@ -56,13 +64,11 @@ export class PremiumComponent implements OnInit { return; } } - - async submit() { - let files: FileList = null; + submit = async () => { + this.licenseForm.markAllAsTouched(); + this.addonForm.markAllAsTouched(); if (this.selfHosted) { - const fileEl = document.getElementById("file") as HTMLInputElement; - files = fileEl.files; - if (files == null || files.length === 0) { + if (this.licenseFile == null) { this.platformUtilsService.showToast( "error", this.i18nService.t("errorOccurred"), @@ -72,53 +78,48 @@ export class PremiumComponent implements OnInit { } } - try { - if (this.selfHosted) { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - if (!this.tokenService.getEmailVerified()) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("verifyEmailFirst"), - ); - return; - } - - const fd = new FormData(); - fd.append("license", files[0]); - this.formPromise = this.apiService.postAccountLicense(fd).then(() => { - return this.finalizePremium(); - }); - } else { - this.formPromise = this.paymentComponent - .createPaymentToken() - .then((result) => { - const fd = new FormData(); - fd.append("paymentMethodType", result[1].toString()); - if (result[0] != null) { - fd.append("paymentToken", result[0]); - } - fd.append("additionalStorageGb", (this.additionalStorage || 0).toString()); - fd.append("country", this.taxInfoComponent.taxInfo.country); - fd.append("postalCode", this.taxInfoComponent.taxInfo.postalCode); - return this.apiService.postPremium(fd); - }) - .then((paymentResponse) => { - if (!paymentResponse.success && paymentResponse.paymentIntentClientSecret != null) { - return this.paymentComponent.handleStripeCardPayment( - paymentResponse.paymentIntentClientSecret, - () => this.finalizePremium(), - ); - } else { - return this.finalizePremium(); - } - }); + if (this.selfHosted) { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + if (!this.tokenService.getEmailVerified()) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("verifyEmailFirst"), + ); + return; } - await this.formPromise; - } catch (e) { - this.logService.error(e); + + const fd = new FormData(); + fd.append("license", this.licenseFile); + await this.apiService.postAccountLicense(fd).then(() => { + return this.finalizePremium(); + }); + } else { + await this.paymentComponent + .createPaymentToken() + .then((result) => { + const fd = new FormData(); + fd.append("paymentMethodType", result[1].toString()); + if (result[0] != null) { + fd.append("paymentToken", result[0]); + } + fd.append("additionalStorageGb", (this.additionalStorage || 0).toString()); + fd.append("country", this.taxInfoComponent.taxInfo.country); + fd.append("postalCode", this.taxInfoComponent.taxInfo.postalCode); + return this.apiService.postPremium(fd); + }) + .then((paymentResponse) => { + if (!paymentResponse.success && paymentResponse.paymentIntentClientSecret != null) { + return this.paymentComponent.handleStripeCardPayment( + paymentResponse.paymentIntentClientSecret, + () => this.finalizePremium(), + ); + } else { + return this.finalizePremium(); + } + }); } - } + }; async finalizePremium() { await this.apiService.refreshIdentityToken(); @@ -127,6 +128,9 @@ export class PremiumComponent implements OnInit { await this.router.navigate(["/settings/subscription/user-subscription"]); } + get additionalStorage(): number { + return this.addonForm.get("additionalStorage").value; + } get additionalStorageTotal(): number { return this.storageGbPrice * Math.abs(this.additionalStorage || 0); } diff --git a/apps/web/src/app/billing/organizations/adjust-subscription.component.html b/apps/web/src/app/billing/organizations/adjust-subscription.component.html index f0200da638..9fe8d20540 100644 --- a/apps/web/src/app/billing/organizations/adjust-subscription.component.html +++ b/apps/web/src/app/billing/organizations/adjust-subscription.component.html @@ -1,65 +1,57 @@ -
-
-
-
- - - + +
+
+ + {{ "subscriptionSeats" | i18n }} + + {{ "total" | i18n }}: {{ additionalSeatCount || 0 }} × {{ seatPrice | currency: "$" }} = {{ adjustedSeatTotal | currency: "$" }} / - {{ interval | i18n }} - -
+ {{ interval | i18n }} +
-
-
-
- - -
- {{ "limitSubscriptionDesc" | i18n }} -
-
-
-
- +
+
+ + + {{ "limitSubscription" | i18n }} + {{ "limitSubscriptionDesc" | i18n }} + +
+
+
+ + {{ "maxSeatLimit" | i18n }} - + {{ "maxSeatCost" | i18n }}: {{ additionalMaxSeatCount || 0 }} × {{ seatPrice | currency: "$" }} = {{ maxSeatTotal | currency: "$" }} / - {{ interval | i18n }} - -
+ {{ interval | i18n }} +
-
+ 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 4290a1281b..b843c79cb9 100644 --- a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts @@ -1,77 +1,102 @@ -import { Component, EventEmitter, Input, Output } from "@angular/core"; +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"; 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"; @Component({ 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; @Input() seatPrice = 0; @Input() interval = "year"; @Output() onAdjusted = new EventEmitter(); + private destroy$ = new Subject(); - formPromise: Promise; - limitSubscription: boolean; - newSeatCount: number; - newMaxSeats: number; - + adjustSubscriptionForm = this.formBuilder.group({ + newSeatCount: [0, [Validators.min(0)]], + limitSubscription: [false], + newMaxSeats: [0, [Validators.min(0)]], + }); + get limitSubscription(): boolean { + return this.adjustSubscriptionForm.value.limitSubscription; + } constructor( private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, - private logService: LogService, private organizationApiService: OrganizationApiServiceAbstraction, + private formBuilder: FormBuilder, ) {} ngOnInit() { - this.limitSubscription = this.maxAutoscaleSeats != null; - this.newSeatCount = this.currentSeatCount; - this.newMaxSeats = this.maxAutoscaleSeats; + this.adjustSubscriptionForm.patchValue({ + newSeatCount: this.currentSeatCount, + limitSubscription: this.maxAutoscaleSeats != null, + newMaxSeats: this.maxAutoscaleSeats, + }); + this.adjustSubscriptionForm + .get("limitSubscription") + .valueChanges.pipe(takeUntil(this.destroy$)) + .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(); + }); } - async submit() { - try { - const request = new OrganizationSubscriptionUpdateRequest( - this.additionalSeatCount, - this.newMaxSeats, - ); - this.formPromise = this.organizationApiService.updatePasswordManagerSeats( - this.organizationId, - request, - ); - - await this.formPromise; - - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("subscriptionUpdated"), - ); - } catch (e) { - this.logService.error(e); + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + submit = async () => { + this.adjustSubscriptionForm.markAllAsTouched(); + if (this.adjustSubscriptionForm.invalid) { + return; } + const request = new OrganizationSubscriptionUpdateRequest( + this.additionalSeatCount, + this.adjustSubscriptionForm.value.newMaxSeats, + ); + await this.organizationApiService.updatePasswordManagerSeats(this.organizationId, request); + + this.platformUtilsService.showToast("success", null, this.i18nService.t("subscriptionUpdated")); + this.onAdjusted.emit(); - } + }; limitSubscriptionChanged() { - if (!this.limitSubscription) { - this.newMaxSeats = null; + if (!this.adjustSubscriptionForm.value.limitSubscription) { + this.adjustSubscriptionForm.value.newMaxSeats = null; } } get additionalSeatCount(): number { - return this.newSeatCount ? this.newSeatCount - this.currentSeatCount : 0; + return this.adjustSubscriptionForm.value.newSeatCount + ? this.adjustSubscriptionForm.value.newSeatCount - this.currentSeatCount + : 0; } get additionalMaxSeatCount(): number { - return this.newMaxSeats ? this.newMaxSeats - this.currentSeatCount : 0; + return this.adjustSubscriptionForm.value.newMaxSeats + ? this.adjustSubscriptionForm.value.newMaxSeats - this.currentSeatCount + : 0; } get adjustedSeatTotal(): number {