[PM-8228] - Create new Premium Component (#10514)

* add new premium component

* finish new premium component

* revert change to config service

* hide copy changes behind feature flag

* revert keys back to original

* remove stateService and translation key

* add missing translation key

* add missing key
This commit is contained in:
Jordan Aasen 2024-08-15 10:31:04 -07:00 committed by GitHub
parent 199ac3de45
commit a72ec8ab89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 185 additions and 12 deletions

View File

@ -1082,6 +1082,9 @@
"ppremiumSignUpStorage": {
"message": "1 GB encrypted storage for file attachments."
},
"premiumSignUpEmergency": {
"message": "Emergency access"
},
"premiumSignUpTwoStepOptions": {
"message": "Proprietary two-step login options such as YubiKey and Duo."
},
@ -1103,6 +1106,9 @@
"premiumPurchaseAlert": {
"message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?"
},
"premiumPurchaseAlertV2": {
"message": "You can purchase Premium from your account settings on the Bitwarden web app."
},
"premiumCurrentMember": {
"message": "You are a Premium member!"
},

View File

@ -0,0 +1,60 @@
<popup-page>
<popup-header slot="header" pageTitle="{{ 'premiumMembership' | i18n }}" showBackButton>
<ng-container slot="end">
<app-pop-out></app-pop-out>
</ng-container>
</popup-header>
<div class="tw-flex tw-flex-col tw-p-2">
<h2 class="tw-font-bold">{{ "premiumFeatures" | i18n }}</h2>
<bit-section>
<bit-card>
<div class="tw-flex tw-flex-col tw-p-3">
<ul class="tw-list-disc tw-pl-5 tw-space-y-2 tw-break-words">
<li>
{{ "ppremiumSignUpStorage" | i18n }}
</li>
<li>
{{ "ppremiumSignUpTwoStepOptions" | i18n }}
</li>
<li>
{{ "premiumSignUpEmergency" | i18n }}
</li>
<li>
{{ "ppremiumSignUpReports" | i18n }}
</li>
<li>
{{ "ppremiumSignUpTotp" | i18n }}
</li>
<li>
{{ "ppremiumSignUpSupport" | i18n }}
</li>
<li>
{{ "ppremiumSignUpFuture" | i18n }}
</li>
</ul>
</div>
<p class="tw-mt-5 tw-mb-0">{{ priceString }}</p>
</bit-card>
</bit-section>
<button bitButton type="submit" buttonType="primary" (click)="purchase()" class="tw-mb-3">
<b>{{ "premiumPurchase" | i18n }}</b>
<i class="bwi bwi-external-link" aria-hidden="true"></i>
</button>
<button
#refreshBtn
type="button"
(click)="refresh()"
[disabled]="$any(refreshBtn).loading"
[appApiAction]="refreshPromise"
bitButton
>
<span [hidden]="$any(refreshBtn).loading">{{ "premiumRefresh" | i18n }}</span>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(refreshBtn).loading"
aria-hidden="true"
></i>
</button>
</div>
</popup-page>

View File

@ -0,0 +1,86 @@
import { CommonModule, CurrencyPipe, Location } from "@angular/common";
import { Component } from "@angular/core";
import { RouterModule } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/vault/components/premium.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
ButtonModule,
CardComponent,
DialogService,
ItemModule,
SectionComponent,
} from "@bitwarden/components";
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
@Component({
selector: "app-premium",
templateUrl: "premium-v2.component.html",
standalone: true,
imports: [
ButtonModule,
CardComponent,
CommonModule,
CurrentAccountComponent,
ItemModule,
JslibModule,
PopupPageComponent,
PopupHeaderComponent,
PopOutComponent,
RouterModule,
SectionComponent,
],
})
export class PremiumV2Component extends BasePremiumComponent {
priceString: string;
constructor(
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
apiService: ApiService,
configService: ConfigService,
logService: LogService,
private location: Location,
private currencyPipe: CurrencyPipe,
dialogService: DialogService,
environmentService: EnvironmentService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) {
super(
i18nService,
platformUtilsService,
apiService,
configService,
logService,
dialogService,
environmentService,
billingAccountProfileStateService,
);
// Support old price string. Can be removed in future once all translations are properly updated.
const thePrice = this.currencyPipe.transform(this.price, "$");
// Safari extension crashes due to $1 appearing in the price string ($10.00). Escape the $ to fix.
const formattedPrice = this.platformUtilsService.isSafari()
? thePrice.replace("$", "$$$")
: thePrice;
this.priceString = i18nService.t("premiumPrice", formattedPrice);
if (this.priceString.indexOf("%price%") > -1) {
this.priceString = this.priceString.replace("%price%", thePrice);
}
}
goBack() {
this.location.back();
}
}

View File

@ -4,11 +4,11 @@ import { Component } from "@angular/core";
import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/vault/components/premium.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { DialogService } from "@bitwarden/components";
@Component({
@ -22,7 +22,7 @@ export class PremiumComponent extends BasePremiumComponent {
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
apiService: ApiService,
stateService: StateService,
configService: ConfigService,
logService: LogService,
private location: Location,
private currencyPipe: CurrencyPipe,
@ -34,8 +34,8 @@ export class PremiumComponent extends BasePremiumComponent {
i18nService,
platformUtilsService,
apiService,
configService,
logService,
stateService,
dialogService,
environmentService,
billingAccountProfileStateService,

View File

@ -46,6 +46,7 @@ import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
import { NotificationsSettingsV1Component } from "../autofill/popup/settings/notifications-v1.component";
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component";
import { PremiumComponent } from "../billing/popup/settings/premium.component";
import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service";
@ -337,12 +338,12 @@ const routes: Routes = [
canActivate: [authGuard],
data: { state: "excluded-domains" },
}),
{
...extensionRefreshSwap(PremiumComponent, PremiumV2Component, {
path: "premium",
component: PremiumComponent,
canActivate: [authGuard],
data: { state: "premium" },
},
}),
...extensionRefreshSwap(AppearanceComponent, AppearanceV2Component, {
path: "appearance",
canActivate: [authGuard],

View File

@ -1174,6 +1174,9 @@
"premiumPurchaseAlert": {
"message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?"
},
"premiumPurchaseAlertV2": {
"message": "You can purchase Premium from your account settings on the Bitwarden web app."
},
"premiumCurrentMember": {
"message": "You are a premium member!"
},

View File

@ -3,6 +3,7 @@ import { Component } from "@angular/core";
import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/vault/components/premium.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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";
@ -19,6 +20,7 @@ export class PremiumComponent extends BasePremiumComponent {
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
apiService: ApiService,
configService: ConfigService,
logService: LogService,
stateService: StateService,
dialogService: DialogService,
@ -29,6 +31,7 @@ export class PremiumComponent extends BasePremiumComponent {
i18nService,
platformUtilsService,
apiService,
configService,
logService,
stateService,
dialogService,

View File

@ -3,12 +3,13 @@ import { firstValueFrom, Observable } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { DialogService } from "@bitwarden/components";
import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
@Directive()
export class PremiumComponent implements OnInit {
@ -16,13 +17,14 @@ export class PremiumComponent implements OnInit {
price = 10;
refreshPromise: Promise<any>;
cloudWebVaultUrl: string;
extensionRefreshFlagEnabled: boolean;
constructor(
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected apiService: ApiService,
protected configService: ConfigService,
private logService: LogService,
protected stateService: StateService,
protected dialogService: DialogService,
private environmentService: EnvironmentService,
billingAccountProfileStateService: BillingAccountProfileStateService,
@ -32,6 +34,9 @@ export class PremiumComponent implements OnInit {
async ngOnInit() {
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
this.extensionRefreshFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
}
async refresh() {
@ -45,11 +50,20 @@ export class PremiumComponent implements OnInit {
}
async purchase() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "premiumPurchase" },
content: { key: "premiumPurchaseAlert" },
const dialogOpts: SimpleDialogOptions = {
title: { key: "continueToBitwardenDotCom" },
content: {
key: this.extensionRefreshFlagEnabled ? "premiumPurchaseAlertV2" : "premiumPurchaseAlert",
},
type: "info",
});
};
if (this.extensionRefreshFlagEnabled) {
dialogOpts.acceptButtonText = { key: "continue" };
dialogOpts.cancelButtonText = { key: "close" };
}
const confirmed = await this.dialogService.openSimpleDialog(dialogOpts);
if (confirmed) {
this.platformUtilsService.launchUri(