mirror of
https://github.com/bitwarden/clients.git
synced 2025-12-22 05:07:16 +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:
@@ -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";
|
||||
EnterPaymentMethodComponent,
|
||||
EnterBillingAddressComponent,
|
||||
PricingCardComponent,
|
||||
BaseCardComponent,
|
||||
],
|
||||
declarations: [
|
||||
SubscriptionComponent,
|
||||
|
||||
@@ -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 }}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
buttonType="secondary"
|
||||
bitLink
|
||||
linkType="primary"
|
||||
(click)="openUploadLicenseDialog()"
|
||||
class="tw-cursor-pointer tw-text-sm"
|
||||
>
|
||||
{{ "purchasePremium" | i18n }}
|
||||
{{ "uploadYourLicenseFile" | i18n }}
|
||||
<i class="bwi bwi-angle-right tw-ml-1" aria-hidden="true"></i>
|
||||
</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>
|
||||
<individual-self-hosting-license-uploader (onLicenseFileUploaded)="onLicenseFileUploaded()" />
|
||||
</bit-section>
|
||||
</bit-container>
|
||||
</div>
|
||||
|
||||
@@ -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 {
|
||||
),
|
||||
);
|
||||
|
||||
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");
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 @@
|
||||
</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">
|
||||
|
||||
@@ -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 {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
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
|
||||
|
||||
@@ -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";
|
||||
standalone: false,
|
||||
})
|
||||
export class UpdateLicenseDialogComponent extends UpdateLicenseComponent {
|
||||
fromUserSubscriptionPage: boolean;
|
||||
|
||||
constructor(
|
||||
private dialogRef: DialogRef,
|
||||
apiService: ApiService,
|
||||
@@ -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 {
|
||||
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 {
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3293,6 +3293,9 @@
|
||||
"launchCloudSubscription": {
|
||||
"message": "Launch Cloud Subscription"
|
||||
},
|
||||
"launchCloudSubscriptionSentenceCase": {
|
||||
"message": "Launch cloud subscription"
|
||||
},
|
||||
"storage": {
|
||||
"message": "Storage"
|
||||
},
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user