Browse Source

Merge 22db9b2253 into 930cb9ab96

pull/17517/merge
cyprain-okeke 8 hours ago committed by GitHub
parent
commit
28ce4bb7a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      apps/web/src/app/billing/individual/individual-billing.module.ts
  2. 127
      apps/web/src/app/billing/individual/premium/self-hosted-premium.component.html
  3. 135
      apps/web/src/app/billing/individual/premium/self-hosted-premium.component.ts
  4. 16
      apps/web/src/app/billing/individual/subscription.component.html
  5. 61
      apps/web/src/app/billing/individual/user-subscription.component.html
  6. 26
      apps/web/src/app/billing/individual/user-subscription.component.ts
  7. 36
      apps/web/src/app/billing/shared/update-license-dialog.component.html
  8. 43
      apps/web/src/app/billing/shared/update-license-dialog.component.ts
  9. 51
      apps/web/src/locales/en/messages.json

2
apps/web/src/app/billing/individual/individual-billing.module.ts

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

127
apps/web/src/app/billing/individual/premium/self-hosted-premium.component.html

@ -1,49 +1,88 @@ @@ -1,49 +1,88 @@
<bit-container>
<bit-section>
<bit-callout type="success">
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
<ul class="bwi-ul">
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpStorage" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpTwoStepOptions" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpEmergency" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpReports" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpTotp" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpSupport" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpFuture" | i18n }}
</li>
</ul>
<div class="tw-max-w-3xl tw-mx-auto">
<bit-section *ngIf="shouldShowUpgradeView$ | async">
<!-- Free Plan Banner -->
<div class="tw-mt-10 tw-mb-4 tw-text-center">
<span bitBadge variant="secondary" [truncate]="false">
{{ "bitwardenFreeplanMessage" | i18n }}
</span>
</div>
<!-- Main Heading -->
<div class="tw-text-center tw-rounded">
<h1 class="tw-mt-2 tw-text-4xl">
{{ "upgradeCompleteSecurity" | i18n }}
</h1>
<p class="tw-text-sm tw-text-muted tw-mb-6 tw-mt-4">
{{ "individualUpgradeDescriptionMessage" | i18n }}
</p>
</div>
<!-- Already have a subscription section -->
<div class="tw-bg-secondary-100 tw-p-4 tw-rounded-lg tw-border tw-border-secondary-300 tw-mb-6">
<p class="tw-font-semibold tw-mb-0.5">
{{ "alreadyHaveSubscriptionQuestion" | i18n }}
</p>
<p class="tw-text-sm tw-text-muted tw-mb-0.5">
{{ "alreadyHaveSubscriptionSelfHostedMessage" | i18n }}
</p>
<a
bitButton
href="{{ cloudPremiumPageUrl$ | async }}"
bitLink
linkType="primary"
(click)="openUploadLicenseDialog()"
class="tw-cursor-pointer tw-text-sm"
>
{{ "uploadYourLicenseFile" | i18n }}
<i class="bwi bwi-angle-right tw-ml-1" aria-hidden="true"></i>
</a>
</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="noreferrer"
buttonType="secondary"
rel="noopener noreferrer"
>
{{ "purchasePremium" | i18n }}
{{ "viewAllPlans" | i18n }}
<i class="bwi bwi-external-link tw-ml-1" aria-hidden="true"></i>
</a>
</bit-callout>
</bit-section>
<bit-section>
<individual-self-hosting-license-uploader (onLicenseFileUploaded)="onLicenseFileUploaded()" />
</div>
</bit-section>
</bit-container>
</div>

135
apps/web/src/app/billing/individual/premium/self-hosted-premium.component.ts

@ -1,36 +1,61 @@ @@ -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 { 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 { 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 { ToastService } from "@bitwarden/components";
import { BillingSharedModule } from "@bitwarden/web-vault/app/billing/shared";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import {
BadgeModule,
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
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
templateUrl: "./self-hosted-premium.component.html",
imports: [SharedModule, BillingSharedModule],
standalone: true,
imports: [
CommonModule,
SectionComponent,
BadgeModule,
TypographyModule,
LinkModule,
I18nPipe,
PricingCardComponent,
],
})
export class SelfHostedPremiumComponent {
cloudPremiumPageUrl$ = this.environmentService.cloudWebVaultUrl$.pipe(
protected cloudPremiumPageUrl$ = this.environmentService.cloudWebVaultUrl$.pipe(
map((url) => `${url}/#/settings/subscription/premium`),
);
hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe(
switchMap((account) =>
account
? this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id)
: of(false),
),
protected cloudFamiliesPageUrl$ = this.environmentService.cloudWebVaultUrl$.pipe(
map((url) => `${url}/#/settings/subscription/premium`),
);
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) =>
account
? this.billingAccountProfileStateService.hasPremiumPersonally$(account.id)
@ -38,42 +63,90 @@ export class SelfHostedPremiumComponent { @@ -38,42 +63,90 @@ export class SelfHostedPremiumComponent {
),
);
onLicenseFileUploaded = async () => {
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("premiumUpdated"),
});
await this.navigateToSubscription();
};
protected shouldShowUpgradeView$: Observable<boolean> = this.hasPremiumPersonally$.pipe(
map((hasPremium) => !hasPremium),
);
protected premiumFeatures = [
this.i18nService.t("builtInAuthenticator"),
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(
private accountService: AccountService,
private activatedRoute: ActivatedRoute,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private dialogService: DialogService,
private environmentService: EnvironmentService,
private i18nService: I18nService,
private router: Router,
private toastService: ToastService,
) {
combineLatest([this.hasPremiumFromAnyOrganization$, this.hasPremiumPersonally$])
// Redirect premium users to subscription page
this.hasPremiumPersonally$
.pipe(
takeUntilDestroyed(),
switchMap(([hasPremiumFromAnyOrganization, hasPremiumPersonally]) => {
if (hasPremiumFromAnyOrganization) {
return this.navigateToVault();
}
takeUntilDestroyed(this.destroyRef),
switchMap((hasPremiumPersonally) => {
if (hasPremiumPersonally) {
return this.navigateToSubscription();
}
return of(true);
}),
)
.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 });
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");
};
}

16
apps/web/src/app/billing/individual/subscription.component.html

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

61
apps/web/src/app/billing/individual/user-subscription.component.html

@ -32,11 +32,6 @@ @@ -32,11 +32,6 @@
{{ "reinstateSubscription" | i18n }}
</button>
</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-gap-16 tw-justify-between tw-w-full">
<div class="tw-flex tw-flex-col">
@ -97,19 +92,49 @@ @@ -97,19 +92,49 @@
</div>
</div>
<ng-container *ngIf="selfHosted">
<div>
<button type="button" bitButton buttonType="secondary" (click)="updateLicense()">
{{ "updateLicense" | i18n }}
</button>
<a
bitButton
buttonType="secondary"
href="{{ this.cloudWebVaultUrl }}/#/settings/subscription"
target="_blank"
rel="noreferrer"
>
{{ "launchCloudSubscription" | i18n }}
</a>
<div class="tw-mt-10 tw-text-center tw-pb-4">
<h1 class="tw-text-4xl tw-my-0">{{ "youHaveBitwardenPremium" | i18n }}</h1>
<div class="tw-text-muted tw-text-xs tw-mb-4 tw-mt-2">
{{ "viewAndManagePremiumSubscription" | i18n }}
</div>
</div>
<div class="tw-flex tw-justify-center">
<bit-base-card class="tw-w-[800px] tw-p-4 sm:tw-p-6">
<div class="tw-flex tw-flex-col tw-gap-5">
<div class="tw-flex tw-items-center tw-justify-between">
<div>
<h2 bitTypography="h2" class="tw-font-semibold tw-mb-0">
{{ "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>
</ng-container>
<div class="tw-max-w-[1340px]" *ngIf="!selfHosted">

26
apps/web/src/app/billing/individual/user-subscription.component.ts

@ -159,7 +159,9 @@ export class UserSubscriptionComponent implements OnInit { @@ -159,7 +159,9 @@ export class UserSubscriptionComponent implements OnInit {
if (this.loading) {
return;
}
const dialogRef = UpdateLicenseDialogComponent.open(this.dialogService);
const dialogRef = UpdateLicenseDialogComponent.open(this.dialogService, {
data: { fromUserSubscriptionPage: true },
});
const result = await lastValueFrom(dialogRef.closed);
if (result === UpdateLicenseDialogResult.Updated) {
await this.load();
@ -259,4 +261,26 @@ export class UserSubscriptionComponent implements OnInit { @@ -259,4 +261,26 @@ export class UserSubscriptionComponent implements OnInit {
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();
}
}

36
apps/web/src/app/billing/shared/update-license-dialog.component.html

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

43
apps/web/src/app/billing/shared/update-license-dialog.component.ts

@ -1,15 +1,28 @@ @@ -1,15 +1,28 @@
import { Component } from "@angular/core";
import { Component, Inject } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
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 { 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 { UpdateLicenseComponent } from "./update-license.component";
export interface UpdateLicenseDialogData {
fromUserSubscriptionPage?: boolean;
}
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
@ -17,6 +30,8 @@ import { UpdateLicenseComponent } from "./update-license.component"; @@ -17,6 +30,8 @@ import { UpdateLicenseComponent } from "./update-license.component";
standalone: false,
})
export class UpdateLicenseDialogComponent extends UpdateLicenseComponent {
fromUserSubscriptionPage: boolean;
constructor(
private dialogRef: DialogRef,
apiService: ApiService,
@ -25,6 +40,9 @@ export class UpdateLicenseDialogComponent extends UpdateLicenseComponent { @@ -25,6 +40,9 @@ export class UpdateLicenseDialogComponent extends UpdateLicenseComponent {
organizationApiService: OrganizationApiServiceAbstraction,
formBuilder: FormBuilder,
toastService: ToastService,
private accountService: AccountService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
@Inject(DIALOG_DATA) private dialogData: UpdateLicenseDialogData = {},
) {
super(
apiService,
@ -34,10 +52,25 @@ export class UpdateLicenseDialogComponent extends UpdateLicenseComponent { @@ -34,10 +52,25 @@ export class UpdateLicenseDialogComponent extends UpdateLicenseComponent {
formBuilder,
toastService,
);
this.fromUserSubscriptionPage = dialogData?.fromUserSubscriptionPage ?? false;
}
async submitLicense() {
const result = await this.submit();
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);
}
}
@ -47,10 +80,10 @@ export class UpdateLicenseDialogComponent extends UpdateLicenseComponent { @@ -47,10 +80,10 @@ export class UpdateLicenseDialogComponent extends UpdateLicenseComponent {
};
cancel = async () => {
await this.cancel();
this.onCanceled.emit();
this.dialogRef.close(UpdateLicenseDialogResult.Cancelled);
};
static open(dialogService: DialogService) {
return dialogService.open<UpdateLicenseDialogResult>(UpdateLicenseDialogComponent);
static open(dialogService: DialogService, config?: DialogConfig<UpdateLicenseDialogData>) {
return dialogService.open<UpdateLicenseDialogResult>(UpdateLicenseDialogComponent, config);
}
}

51
apps/web/src/locales/en/messages.json

@ -3293,6 +3293,9 @@ @@ -3293,6 +3293,9 @@
"launchCloudSubscription": {
"message": "Launch Cloud Subscription"
},
"launchCloudSubscriptionSentenceCase": {
"message": "Launch cloud subscription"
},
"storage": {
"message": "Storage"
},
@ -12429,5 +12432,53 @@ @@ -12429,5 +12432,53 @@
},
"whyAmISeeingThis": {
"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"
}
}

Loading…
Cancel
Save