1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-06 02:23:44 +00:00

[PM 27122] Individual subscription page for self-hosted customers (#17517)

* implement the self-host subscription changes

* Correct few ui changes

* Update to h1

* PR review changes

* Changes for the async cancel

* Resolve the two bug issues

* implement the review comments

* Resolve the Active issue

* Fix the space issues

* Remove the tabs for billing and payment

* revert the self-host changes

* Fix the subtitle issue
This commit is contained in:
cyprain-okeke
2025-12-17 21:13:18 +01:00
committed by GitHub
parent 5504d49751
commit bcbf013cd9
9 changed files with 381 additions and 118 deletions

View File

@@ -1,5 +1,6 @@
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { BaseCardComponent } from "@bitwarden/components";
import { PricingCardComponent } from "@bitwarden/pricing"; import { PricingCardComponent } from "@bitwarden/pricing";
import { import {
EnterBillingAddressComponent, EnterBillingAddressComponent,
@@ -23,6 +24,7 @@ import { UserSubscriptionComponent } from "./user-subscription.component";
EnterPaymentMethodComponent, EnterPaymentMethodComponent,
EnterBillingAddressComponent, EnterBillingAddressComponent,
PricingCardComponent, PricingCardComponent,
BaseCardComponent,
], ],
declarations: [ declarations: [
SubscriptionComponent, SubscriptionComponent,

View File

@@ -1,49 +1,88 @@
<bit-container> <div class="tw-max-w-3xl tw-mx-auto">
<bit-section> <bit-section *ngIf="shouldShowUpgradeView$ | async">
<bit-callout type="success"> <!-- Free Plan Banner -->
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p> <div class="tw-mt-10 tw-mb-4 tw-text-center">
<ul class="bwi-ul"> <span bitBadge variant="secondary" [truncate]="false">
<li> {{ "bitwardenFreeplanMessage" | i18n }}
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i> </span>
{{ "premiumSignUpStorage" | i18n }} </div>
</li>
<li> <!-- Main Heading -->
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i> <div class="tw-text-center tw-rounded">
{{ "premiumSignUpTwoStepOptions" | i18n }} <h1 class="tw-mt-2 tw-text-4xl">
</li> {{ "upgradeCompleteSecurity" | i18n }}
<li> </h1>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i> <p class="tw-text-sm tw-text-muted tw-mb-6 tw-mt-4">
{{ "premiumSignUpEmergency" | i18n }} {{ "individualUpgradeDescriptionMessage" | i18n }}
</li> </p>
<li> </div>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpReports" | i18n }} <!-- Already have a subscription section -->
</li> <div class="tw-bg-secondary-100 tw-p-4 tw-rounded-lg tw-border tw-border-secondary-300 tw-mb-6">
<li> <p class="tw-font-semibold tw-mb-0.5">
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i> {{ "alreadyHaveSubscriptionQuestion" | i18n }}
{{ "premiumSignUpTotp" | i18n }} </p>
</li> <p class="tw-text-sm tw-text-muted tw-mb-0.5">
<li> {{ "alreadyHaveSubscriptionSelfHostedMessage" | i18n }}
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i> </p>
{{ "premiumSignUpSupport" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpFuture" | i18n }}
</li>
</ul>
<a <a
bitButton bitLink
href="{{ cloudPremiumPageUrl$ | async }}" linkType="primary"
target="_blank" (click)="openUploadLicenseDialog()"
rel="noreferrer" class="tw-cursor-pointer tw-text-sm"
buttonType="secondary"
> >
{{ "purchasePremium" | i18n }} {{ "uploadYourLicenseFile" | i18n }}
<i class="bwi bwi-angle-right tw-ml-1" aria-hidden="true"></i>
</a> </a>
</bit-callout> </div>
<!-- Two-Card Layout -->
<div class="tw-grid tw-grid-cols-1 md:tw-grid-cols-2 tw-gap-6 tw-mt-6 tw-justify-center">
<!-- Premium Card -->
<div>
<billing-pricing-card
[tagline]="'planDescPremium' | i18n"
[button]="{
type: 'primary',
text: ('upgradeToPremium' | i18n),
icon: { type: 'bwi-external-link', position: 'after' },
}"
[features]="premiumFeatures"
(buttonClick)="onPremiumUpgradeClick()"
>
<h3 slot="title" bitTypography="h3" class="tw-m-0">{{ "premium" | i18n }}</h3>
</billing-pricing-card>
</div>
<!-- Families Card -->
<div>
<billing-pricing-card
[tagline]="'planDescFamiliesV2' | i18n"
[button]="{
type: 'secondary',
text: ('upgradeToFamilies' | i18n),
icon: { type: 'bwi-external-link', position: 'after' },
}"
[features]="familiesFeatures"
(buttonClick)="onFamiliesUpgradeClick()"
>
<h3 slot="title" bitTypography="h3" class="tw-m-0">{{ "families" | i18n }}</h3>
</billing-pricing-card>
</div>
</div>
<!-- View all plans Link -->
<div class="tw-text-center tw-mt-6">
<a
bitLink
linkType="primary"
href="https://bitwarden.com/pricing/"
target="_blank"
rel="noopener noreferrer"
>
{{ "viewAllPlans" | i18n }}
<i class="bwi bwi-external-link tw-ml-1" aria-hidden="true"></i>
</a>
</div>
</bit-section> </bit-section>
<bit-section> </div>
<individual-self-hosting-license-uploader (onLicenseFileUploaded)="onLicenseFileUploaded()" />
</bit-section>
</bit-container>

View File

@@ -1,36 +1,61 @@
import { Component } from "@angular/core"; import { CommonModule } from "@angular/common";
import { Component, DestroyRef, inject } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { combineLatest, map, of, switchMap } from "rxjs"; import { firstValueFrom, lastValueFrom, map, Observable, of, switchMap } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ToastService } from "@bitwarden/components"; import {
import { BillingSharedModule } from "@bitwarden/web-vault/app/billing/shared"; BadgeModule,
import { SharedModule } from "@bitwarden/web-vault/app/shared"; DialogService,
LinkModule,
SectionComponent,
ToastService,
TypographyModule,
} from "@bitwarden/components";
import { PricingCardComponent } from "@bitwarden/pricing";
import { I18nPipe } from "@bitwarden/ui-common";
import { UpdateLicenseDialogComponent } from "../../shared/update-license-dialog.component";
import { UpdateLicenseDialogResult } from "../../shared/update-license-types";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({ @Component({
templateUrl: "./self-hosted-premium.component.html", templateUrl: "./self-hosted-premium.component.html",
imports: [SharedModule, BillingSharedModule], standalone: true,
imports: [
CommonModule,
SectionComponent,
BadgeModule,
TypographyModule,
LinkModule,
I18nPipe,
PricingCardComponent,
],
}) })
export class SelfHostedPremiumComponent { export class SelfHostedPremiumComponent {
cloudPremiumPageUrl$ = this.environmentService.cloudWebVaultUrl$.pipe( protected cloudPremiumPageUrl$ = this.environmentService.cloudWebVaultUrl$.pipe(
map((url) => `${url}/#/settings/subscription/premium`), map((url) => `${url}/#/settings/subscription/premium`),
); );
hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe( protected cloudFamiliesPageUrl$ = this.environmentService.cloudWebVaultUrl$.pipe(
switchMap((account) => map((url) => `${url}/#/settings/subscription/premium`),
account
? this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id)
: of(false),
),
); );
hasPremiumPersonally$ = this.accountService.activeAccount$.pipe( protected hasPremiumFromAnyOrganization$: Observable<boolean> =
this.accountService.activeAccount$.pipe(
switchMap((account) =>
account
? this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id)
: of(false),
),
);
protected hasPremiumPersonally$: Observable<boolean> = this.accountService.activeAccount$.pipe(
switchMap((account) => switchMap((account) =>
account account
? this.billingAccountProfileStateService.hasPremiumPersonally$(account.id) ? this.billingAccountProfileStateService.hasPremiumPersonally$(account.id)
@@ -38,42 +63,90 @@ export class SelfHostedPremiumComponent {
), ),
); );
onLicenseFileUploaded = async () => { protected shouldShowUpgradeView$: Observable<boolean> = this.hasPremiumPersonally$.pipe(
this.toastService.showToast({ map((hasPremium) => !hasPremium),
variant: "success", );
title: "",
message: this.i18nService.t("premiumUpdated"), protected premiumFeatures = [
}); this.i18nService.t("builtInAuthenticator"),
await this.navigateToSubscription(); this.i18nService.t("secureFileStorage"),
}; this.i18nService.t("emergencyAccess"),
this.i18nService.t("breachMonitoring"),
this.i18nService.t("andMoreFeatures"),
];
protected familiesFeatures = [
this.i18nService.t("premiumAccounts"),
this.i18nService.t("familiesUnlimitedSharing"),
this.i18nService.t("familiesUnlimitedCollections"),
this.i18nService.t("familiesSharedStorage"),
];
private destroyRef = inject(DestroyRef);
constructor( constructor(
private accountService: AccountService, private accountService: AccountService,
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
private dialogService: DialogService,
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private i18nService: I18nService, private i18nService: I18nService,
private router: Router, private router: Router,
private toastService: ToastService, private toastService: ToastService,
) { ) {
combineLatest([this.hasPremiumFromAnyOrganization$, this.hasPremiumPersonally$]) // Redirect premium users to subscription page
this.hasPremiumPersonally$
.pipe( .pipe(
takeUntilDestroyed(), takeUntilDestroyed(this.destroyRef),
switchMap(([hasPremiumFromAnyOrganization, hasPremiumPersonally]) => { switchMap((hasPremiumPersonally) => {
if (hasPremiumFromAnyOrganization) {
return this.navigateToVault();
}
if (hasPremiumPersonally) { if (hasPremiumPersonally) {
return this.navigateToSubscription(); return this.navigateToSubscription();
} }
return of(true); return of(true);
}), }),
) )
.subscribe(); .subscribe();
} }
navigateToSubscription = () => protected openUploadLicenseDialog = async () => {
const dialogRef = UpdateLicenseDialogComponent.open(this.dialogService);
const result = await lastValueFrom(dialogRef.closed);
if (result === UpdateLicenseDialogResult.Updated) {
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("premiumUpdated"),
});
await this.navigateToSubscription();
}
};
protected navigateToSubscription = async (): Promise<boolean> =>
this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute }); this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute });
navigateToVault = () => this.router.navigate(["/vault"]);
protected onPremiumUpgradeClick = async () => {
const url = await firstValueFrom(this.cloudPremiumPageUrl$);
if (!url) {
this.toastService.showToast({
variant: "error",
title: "",
message: this.i18nService.t("cloudUrlNotConfigured"),
});
return;
}
window.open(url, "_blank", "noopener,noreferrer");
};
protected onFamiliesUpgradeClick = async () => {
const url = await firstValueFrom(this.cloudFamiliesPageUrl$);
if (!url) {
this.toastService.showToast({
variant: "error",
title: "",
message: this.i18nService.t("cloudUrlNotConfigured"),
});
return;
}
window.open(url, "_blank", "noopener,noreferrer");
};
} }

View File

@@ -1,11 +1,13 @@
<app-header> <app-header>
<bit-tab-nav-bar slot="tabs" *ngIf="!selfHosted"> @if (!selfHosted) {
<bit-tab-link [route]="(hasPremium$ | async) ? 'user-subscription' : 'premium'">{{ <bit-tab-nav-bar slot="tabs">
"subscription" | i18n <bit-tab-link [route]="(hasPremium$ | async) ? 'user-subscription' : 'premium'">{{
}}</bit-tab-link> "subscription" | i18n
<bit-tab-link route="payment-details">{{ "paymentDetails" | i18n }}</bit-tab-link> }}</bit-tab-link>
<bit-tab-link route="billing-history">{{ "billingHistory" | i18n }}</bit-tab-link> <bit-tab-link route="payment-details">{{ "paymentDetails" | i18n }}</bit-tab-link>
</bit-tab-nav-bar> <bit-tab-link route="billing-history">{{ "billingHistory" | i18n }}</bit-tab-link>
</bit-tab-nav-bar>
}
</app-header> </app-header>
<router-outlet></router-outlet> <router-outlet></router-outlet>

View File

@@ -32,11 +32,6 @@
{{ "reinstateSubscription" | i18n }} {{ "reinstateSubscription" | i18n }}
</button> </button>
</bit-callout> </bit-callout>
<dl *ngIf="selfHosted">
<dt>{{ "expiration" | i18n }}</dt>
<dd *ngIf="sub.expiration">{{ sub.expiration | date: "mediumDate" }}</dd>
<dd *ngIf="!sub.expiration">{{ "neverExpires" | i18n }}</dd>
</dl>
<div class="tw-flex tw-max-w-[1340px] tw-pt-6" *ngIf="!selfHosted"> <div class="tw-flex tw-max-w-[1340px] tw-pt-6" *ngIf="!selfHosted">
<div class="tw-flex tw-gap-16 tw-justify-between tw-w-full"> <div class="tw-flex tw-gap-16 tw-justify-between tw-w-full">
<div class="tw-flex tw-flex-col"> <div class="tw-flex tw-flex-col">
@@ -97,19 +92,49 @@
</div> </div>
</div> </div>
<ng-container *ngIf="selfHosted"> <ng-container *ngIf="selfHosted">
<div> <div class="tw-mt-10 tw-text-center tw-pb-4">
<button type="button" bitButton buttonType="secondary" (click)="updateLicense()"> <h1 class="tw-text-4xl tw-my-0">{{ "youHaveBitwardenPremium" | i18n }}</h1>
{{ "updateLicense" | i18n }} <div class="tw-text-muted tw-text-xs tw-mb-4 tw-mt-2">
</button> {{ "viewAndManagePremiumSubscription" | i18n }}
<a </div>
bitButton </div>
buttonType="secondary" <div class="tw-flex tw-justify-center">
href="{{ this.cloudWebVaultUrl }}/#/settings/subscription" <bit-base-card class="tw-w-[800px] tw-p-4 sm:tw-p-6">
target="_blank" <div class="tw-flex tw-flex-col tw-gap-5">
rel="noreferrer" <div class="tw-flex tw-items-center tw-justify-between">
> <div>
{{ "launchCloudSubscription" | i18n }} <h2 bitTypography="h2" class="tw-font-semibold tw-mb-0">
</a> {{ "premiumMembership" | i18n }}
</h2>
</div>
<span bitBadge variant="success" *ngIf="isSubscriptionActive">{{
"active" | i18n
}}</span>
</div>
<p bitTypography="body1" class="tw-m-0" *ngIf="sub.expiration">
{{ "youNeedToUpdateLicenseFile" | i18n }}
<strong>{{ sub.expiration | date: "MMMM d, y" }}</strong
>.
</p>
<div class="tw-flex tw-gap-4">
<button type="button" bitButton buttonType="secondary" (click)="updateLicense()">
{{ "updateLicense" | i18n }}
</button>
<a
bitButton
buttonType="secondary"
href="{{ this.cloudWebVaultUrl }}/#/settings/subscription"
target="_blank"
rel="noreferrer"
>
{{ "launchCloudSubscriptionSentenceCase" | i18n }}
<i class="bwi bwi-external-link tw-ml-1" aria-hidden="true"></i>
</a>
</div>
</div>
</bit-base-card>
</div> </div>
</ng-container> </ng-container>
<div class="tw-max-w-[1340px]" *ngIf="!selfHosted"> <div class="tw-max-w-[1340px]" *ngIf="!selfHosted">

View File

@@ -159,7 +159,9 @@ export class UserSubscriptionComponent implements OnInit {
if (this.loading) { if (this.loading) {
return; return;
} }
const dialogRef = UpdateLicenseDialogComponent.open(this.dialogService); const dialogRef = UpdateLicenseDialogComponent.open(this.dialogService, {
data: { fromUserSubscriptionPage: true },
});
const result = await lastValueFrom(dialogRef.closed); const result = await lastValueFrom(dialogRef.closed);
if (result === UpdateLicenseDialogResult.Updated) { if (result === UpdateLicenseDialogResult.Updated) {
await this.load(); await this.load();
@@ -259,4 +261,26 @@ export class UserSubscriptionComponent implements OnInit {
amountOff: discount.amountOff, amountOff: discount.amountOff,
}; };
} }
get isSubscriptionActive(): boolean {
if (!this.sub) {
return false;
}
if (this.selfHosted) {
return true;
}
const expiration = this.sub.expiration;
if (!expiration || expiration.trim() === "") {
return true;
}
const expirationDate = new Date(expiration);
if (isNaN(expirationDate.getTime())) {
return true;
}
return expirationDate > new Date();
}
} }

View File

@@ -1,16 +1,30 @@
<form [formGroup]="updateLicenseForm" [bitSubmit]="submitLicenseDialog"> <form [formGroup]="updateLicenseForm" [bitSubmit]="submitLicenseDialog">
<bit-dialog dialogSize="default" [title]="'updateLicense' | i18n"> <bit-dialog
dialogSize="default"
[title]="(fromUserSubscriptionPage ? 'uploadLicense' : 'uploadLicenseFile') | i18n"
>
<ng-container bitDialogContent> <ng-container bitDialogContent>
<bit-form-field> <p class="tw-mb-4">{{ "uploadLicenseFileDesc" | i18n: "bitwarden_license.json" }}</p>
<bit-label>{{ "licenseFile" | i18n }}</bit-label> <div class="tw-mb-4">
<div> <label class="tw-block tw-text-sm tw-text-muted tw-mb-2">{{
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()"> (fromUserSubscriptionPage ? "uploadYourPremiumLicenseFile" : "uploadYourLicenseFile")
| i18n
}}</label>
<div class="tw-mb-2">
<button
bitButton
type="button"
buttonType="unstyled"
class="tw-text-primary-600 tw-p-0 tw-border-0 tw-bg-transparent hover:tw-underline tw-cursor-pointer"
(click)="fileSelector.click()"
>
{{ "chooseFile" | i18n }} {{ "chooseFile" | i18n }}
</button> </button>
{{ licenseFile ? licenseFile.name : ("noFileChosen" | i18n) }} <span class="tw-ml-2 tw-text-muted">{{
licenseFile ? licenseFile.name : ("noFileChosen" | i18n)
}}</span>
</div> </div>
<input <input
bitInput
#fileSelector #fileSelector
type="file" type="file"
formControlName="file" formControlName="file"
@@ -18,12 +32,12 @@
hidden hidden
class="tw-hidden" class="tw-hidden"
/> />
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }}</bit-hint> <p class="tw-text-sm tw-text-muted">{{ "maxFileSizeSansPunctuation" | i18n }}</p>
</bit-form-field> </div>
</ng-container> </ng-container>
<ng-container bitDialogFooter> <ng-container bitDialogFooter>
<button type="submit" buttonType="primary" bitButton bitFormButton> <button type="submit" buttonType="primary" bitButton bitFormButton [disabled]="!licenseFile">
{{ "submit" | i18n }} {{ "upload" | i18n }}
</button> </button>
<button <button
bitButton bitButton

View File

@@ -1,15 +1,28 @@
import { Component } from "@angular/core"; import { Component, Inject } from "@angular/core";
import { FormBuilder } from "@angular/forms"; import { FormBuilder } from "@angular/forms";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; import {
DIALOG_DATA,
DialogConfig,
DialogRef,
DialogService,
ToastService,
} from "@bitwarden/components";
import { UpdateLicenseDialogResult } from "./update-license-types"; import { UpdateLicenseDialogResult } from "./update-license-types";
import { UpdateLicenseComponent } from "./update-license.component"; import { UpdateLicenseComponent } from "./update-license.component";
export interface UpdateLicenseDialogData {
fromUserSubscriptionPage?: boolean;
}
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({ @Component({
@@ -17,6 +30,8 @@ import { UpdateLicenseComponent } from "./update-license.component";
standalone: false, standalone: false,
}) })
export class UpdateLicenseDialogComponent extends UpdateLicenseComponent { export class UpdateLicenseDialogComponent extends UpdateLicenseComponent {
fromUserSubscriptionPage: boolean;
constructor( constructor(
private dialogRef: DialogRef, private dialogRef: DialogRef,
apiService: ApiService, apiService: ApiService,
@@ -25,6 +40,9 @@ export class UpdateLicenseDialogComponent extends UpdateLicenseComponent {
organizationApiService: OrganizationApiServiceAbstraction, organizationApiService: OrganizationApiServiceAbstraction,
formBuilder: FormBuilder, formBuilder: FormBuilder,
toastService: ToastService, toastService: ToastService,
private accountService: AccountService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
@Inject(DIALOG_DATA) private dialogData: UpdateLicenseDialogData = {},
) { ) {
super( super(
apiService, apiService,
@@ -34,10 +52,25 @@ export class UpdateLicenseDialogComponent extends UpdateLicenseComponent {
formBuilder, formBuilder,
toastService, toastService,
); );
this.fromUserSubscriptionPage = dialogData?.fromUserSubscriptionPage ?? false;
} }
async submitLicense() { async submitLicense() {
const result = await this.submit(); const result = await this.submit();
if (result === UpdateLicenseDialogResult.Updated) { if (result === UpdateLicenseDialogResult.Updated) {
// Update billing state after successful upload (only for personal licenses)
if (this.organizationId == null) {
const account: Account | null = await firstValueFrom(this.accountService.activeAccount$);
if (account) {
const hasPremiumFromAnyOrganization = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id),
);
await this.billingAccountProfileStateService.setHasPremium(
true,
hasPremiumFromAnyOrganization,
account.id,
);
}
}
this.dialogRef.close(UpdateLicenseDialogResult.Updated); this.dialogRef.close(UpdateLicenseDialogResult.Updated);
} }
} }
@@ -47,10 +80,10 @@ export class UpdateLicenseDialogComponent extends UpdateLicenseComponent {
}; };
cancel = async () => { cancel = async () => {
await this.cancel(); this.onCanceled.emit();
this.dialogRef.close(UpdateLicenseDialogResult.Cancelled); this.dialogRef.close(UpdateLicenseDialogResult.Cancelled);
}; };
static open(dialogService: DialogService) { static open(dialogService: DialogService, config?: DialogConfig<UpdateLicenseDialogData>) {
return dialogService.open<UpdateLicenseDialogResult>(UpdateLicenseDialogComponent); return dialogService.open<UpdateLicenseDialogResult>(UpdateLicenseDialogComponent, config);
} }
} }

View File

@@ -3293,6 +3293,9 @@
"launchCloudSubscription": { "launchCloudSubscription": {
"message": "Launch Cloud Subscription" "message": "Launch Cloud Subscription"
}, },
"launchCloudSubscriptionSentenceCase": {
"message": "Launch cloud subscription"
},
"storage": { "storage": {
"message": "Storage" "message": "Storage"
}, },
@@ -12429,5 +12432,53 @@
}, },
"whyAmISeeingThis": { "whyAmISeeingThis": {
"message": "Why am I seeing this?" "message": "Why am I seeing this?"
},
"youHaveBitwardenPremium": {
"message": "You have Bitwarden Premium"
},
"viewAndManagePremiumSubscription": {
"message": "View and manage your Premium subscription"
},
"youNeedToUpdateLicenseFile": {
"message": "You'll need to update your license file"
},
"youNeedToUpdateLicenseFileDate": {
"message": "$DATE$.",
"placeholders": {
"date": {
"content": "$1",
"example": "June 12, 2026"
}
}
},
"uploadLicenseFile": {
"message": "Upload license file"
},
"uploadYourLicenseFile": {
"message": "Upload your license file"
},
"uploadYourPremiumLicenseFile": {
"message": "Upload your Premium license file"
},
"uploadLicenseFileDesc": {
"message": "Your license file name will be similar to: $FILE_NAME$",
"placeholders": {
"file_name": {
"content": "$1",
"example": "bitwarden_license.json"
}
}
},
"alreadyHaveSubscriptionQuestion": {
"message": "Already have a subscription?"
},
"alreadyHaveSubscriptionSelfHostedMessage": {
"message": "Open the subscription page on your Bitwarden cloud account and download your license file. Then return to this screen and upload it below."
},
"viewAllPlans": {
"message": "View all plans"
},
"planDescPremium":{
"message": "Complete online security"
} }
} }