1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-04 01:23:57 +00:00

Price and Plan Updates (#598)

* added the multi select checkbox to org ciphers

* wired up select all/none

* allowed for bulk delete of ciphers from the org vault

* refactored bulk actions into a dedicated component

* tweaked formatting settings and reformatted files

* moved some shared code to jslib

* some more formatting fixes

* undid jslib connection changes

* removed a function that was moved to jslib

* reset jslib again?

* set up delete many w/admin cipher methods

* removed extra href tags

* added organization id to bulk delete request model when coming from an org vault

* fixed up some compiler warnings for formatting

* updated organization create component to pull list of plans from static store

* wired up the organization create page to new data struct

* continued work on plan updates

* accounted for the subscription screen in plan updates

* adjusted for code review changes from server PR for plan updates

* cleaned up linter errors

* changed a few variable names

* moved price information, added sales tax and subtotal labels

* code review fixups for bulk delete from org vault

* added back a removed parameter from the vault component

* seperated some imports with newlines

* updated jslib

* resolved some build errors

* updated names to reflect server name changes for plan updates

* adjusted logic for using annual total for annual prices in server model

* rearranged an import for the linter

* broke up an async call

* updated organization create component to pull list of plans from static store

* wired up the organization create page to new data struct

* continued work on plan updates

* accounted for the subscription screen in plan updates

* adjusted for code review changes from server PR for plan updates

* cleaned up linter errors

* changed a few variable names

* moved price information, added sales tax and subtotal labels

* updated names to reflect server name changes for plan updates

* adjusted logic for using annual total for annual prices in server model

* rearranged an import for the linter

* broke up an async call

* resolved merge fun

* updated jslib

* made plans a public variable

* removed sales tax hooks

* added a getter for selected plan interval

* went a little too crazy with the interval getter

* formatting

* added a semicolon

* updated jslib

Co-authored-by: Addison Beck <addisonbeck@MacBook-Pro.local>
This commit is contained in:
Addison Beck
2020-08-12 17:16:38 -04:00
committed by GitHub
parent c46af91240
commit 5f04950358
4 changed files with 280 additions and 291 deletions

View File

@@ -1,3 +1,7 @@
<ng-container *ngIf="loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</ng-container>
<ng-container *ngIf="createOrganization && selfHosted">
<p>{{'uploadLicenseFileOrg' | i18n}}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
@@ -13,7 +17,8 @@
</button>
</form>
</ng-container>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="!selfHosted">
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate
*ngIf="!loading && !selfHosted && this.plans">
<h2 class="mt-5">{{'generalInformation' | i18n}}</h2>
<div class="row" *ngIf="createOrganization">
<div class="form-group col-6">
@@ -38,69 +43,53 @@
</div>
</div>
<h2 class="mt-5">{{'chooseYourPlan' | i18n}}</h2>
<div class="form-check form-check-block" *ngIf="!ownedBusiness && showFree">
<input class="form-check-input" type="radio" name="PlanType" id="planFree" value="free" [(ngModel)]="plan"
(change)="changedPlan()">
<label class="form-check-label" for="planFree">
{{'planNameFree' | i18n}}
<small class="mb-1">{{'planDescFree' | i18n : '1'}}</small>
<small>• {{'limitedUsers' | i18n : '2'}}</small>
<small> {{'limitedCollections' | i18n : '2'}}</small>
<span>{{'freeForever' | i18n}}</span>
<div *ngFor="let selectableProduct of selectableProducts" class="form-check form-check-block">
<input class="form-check-input" type="radio" name="product" id="product{{selectableProduct.product}}"
[value]="selectableProduct.product" [(ngModel)]="product" (change)="changedProduct()">
<label class="form-check-label" for="product">
{{ selectableProduct.nameLocalizationKey | i18n}}
<small class="mb-1">{{ selectableProduct.descriptionLocalizationKey | i18n : '1'}}</small>
<small *ngIf="selectableProduct.product == productTypes.Free">
{{'limitedUsers' | i18n : selectableProduct.maxUsers }}</small>
<small *ngIf="selectableProduct.product != productTypes.Free && selectableProduct.maxUsers">
{{'addShareLimitedUsers' | i18n : selectableProduct.maxUsers}}</small>
<small *ngIf="!selectableProduct.maxUsers">
{{'addShareUnlimitedUsers' | i18n}}</small>
<small *ngIf="selectableProduct.maxCollections">
{{'limitedCollections' | i18n : selectableProduct.maxCollections }}</small>
<small *ngIf="selectableProduct.maxAdditionalSeats">
{{'addShareLimitedUsers' | i18n : selectableProduct.maxAdditionalSeats }}</small>
<small *ngIf="!selectableProduct.maxCollections">• {{'createUnlimitedCollections' | i18n}}</small>
<small *ngIf="selectableProduct.baseStorageGb">
{{'gbEncryptedFileStorage' | i18n : selectableProduct.baseStorageGb + 'GB'}}</small>
<small *ngIf="selectableProduct.hasGroups">• {{'controlAccessWithGroups' | i18n}}</small>
<small *ngIf="selectableProduct.hasApi">• {{'trackAuditLogs' | i18n}}</small>
<small *ngIf="selectableProduct.hasDirectory">• {{'syncUsersFromDirectory' | i18n}}</small>
<small *ngIf="selectableProduct.hasSelfHost">• {{'onPremHostingOptional' | i18n}}</small>
<small *ngIf="selectableProduct.usersGetPremium">• {{'usersGetPremium' | i18n}}</small>
<small *ngIf="selectableProduct.hasSso">• sso blurb here</small>
<small *ngIf="selectableProduct.product != productTypes.Free">• {{'priorityCustomerSupport' | i18n}}</small>
<small *ngIf="selectableProduct.trialPeriodDays">
{{'xDayFreeTrial' | i18n : selectableProduct.trialPeriodDays }}
</small>
<span *ngIf="selectableProduct.product != productTypes.Free">
<ng-container *ngIf="selectableProduct.basePrice">
{{selectableProduct.basePrice / 12 | currency:'$'}} /{{'month' | i18n}},
{{'includesXUsers' | i18n : selectableProduct.baseSeats}}
<ng-container *ngIf="selectableProduct.hasAdditionalSeatsOption">
{{('additionalUsers' | i18n).toLowerCase()}}
{{selectableProduct.seatPrice / 12| currency:'$'}} /{{'month' | i18n}}
</ng-container>
</ng-container>
</span>
<span *ngIf="!selectableProduct.basePrice && selectableProduct.hasAdditionalSeatsOption">
{{'costPerUser' | i18n : (selectableProduct.seatPrice / 12 | currency:'$')}} /{{'month' | i18n}}
</span>
<span *ngIf="selectableProduct.product == productTypes.Free">{{'freeForever' | i18n}}</span>
</label>
</div>
<div class="form-check form-check-block" *ngIf="!ownedBusiness">
<input class="form-check-input" type="radio" name="PlanType" id="planFamilies" value="families"
[(ngModel)]="plan" (change)="changedPlan()">
<label class="form-check-label" for="planFamilies">
{{'planNameFamilies' | i18n}}
<small class="mb-1">{{'planDescFamilies' | i18n}}</small>
<small>• {{'addShareLimitedUsers' | i18n : '5'}}</small>
<small>• {{'createUnlimitedCollections' | i18n}}</small>
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
<small>• {{'onPremHostingOptional' | i18n}}</small>
<small>• {{'priorityCustomerSupport' | i18n}}</small>
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
<span>{{1 | currency:'$'}} /{{'month' | i18n}}, {{'includesXUsers' | i18n : 5}}</span>
</label>
</div>
<div class="form-check form-check-block">
<input class="form-check-input" type="radio" name="PlanType" id="planTeams" value="teams" [(ngModel)]="plan"
(change)="changedPlan()">
<label class="form-check-label" for="planTeams">
{{'planNameTeams' | i18n}}
<small class="mb-1">{{'planDescTeams' | i18n}}</small>
<small>• {{'addShareUnlimitedUsers' | i18n}}</small>
<small>• {{'createUnlimitedCollections' | i18n}}</small>
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
<small>• {{'priorityCustomerSupport' | i18n}}</small>
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
<span>{{5 | currency:'$'}} /{{'month' | i18n}}, {{'includesXUsers' | i18n : 5}},
{{('additionalUsers' | i18n).toLowerCase()}}
{{2 | currency:'$'}} /{{'month' | i18n}}</span>
</label>
</div>
<div class="form-check form-check-block">
<input class="form-check-input" type="radio" name="PlanType" id="planEnterprise" value="enterprise"
[(ngModel)]="plan" (change)="changedPlan()">
<label class="form-check-label" for="planEnterprise">
{{'planNameEnterprise' | i18n}}
<small class="mb-1">{{'planDescEnterprise' | i18n}}</small>
<small>• {{'addShareUnlimitedUsers' | i18n}}</small>
<small>• {{'createUnlimitedCollections' | i18n}}</small>
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
<small>• {{'controlAccessWithGroups' | i18n}}</small>
<small>• {{'trackAuditLogs' | i18n}}</small>
<small>• {{'syncUsersFromDirectory' | i18n}}</small>
<small>• {{'onPremHostingOptional' | i18n}}</small>
<small>• {{'usersGetPremium' | i18n}}</small>
<small>• {{'priorityCustomerSupport' | i18n}}</small>
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
<span>{{'costPerUser' | i18n : (3 | currency:'$')}} /{{'month' | i18n}}</span>
</label>
</div>
<ng-container *ngIf="!plans[plan].noPayment">
<ng-container *ngIf="!plans[plan].noAdditionalSeats && !plans[plan].baseSeats">
<div *ngIf="product !== productTypes.Free">
<ng-container *ngIf="selectedPlan.hasAdditionalSeatsOption && !selectedPlan.baseSeats">
<h2 class="mt-5">{{'users' | i18n}}</h2>
<div class="row">
<div class="col-6">
@@ -113,13 +102,13 @@
</div>
</ng-container>
<h2 class="mt-5">{{'addons' | i18n}}</h2>
<div class="row" *ngIf="!plans[plan].noAdditionalSeats && plans[plan].baseSeats">
<div class="row" *ngIf="selectedPlan.hasAdditionalSeatsOption && selectedPlan.baseSeats">
<div class="form-group col-6">
<label for="additionalSeats">{{'additionalUserSeats' | i18n}}</label>
<input id="additionalSeats" class="form-control" type="number" name="AdditionalSeats"
[(ngModel)]="additionalSeats" min="0" max="100000" placeholder="{{'userSeatsDesc' | i18n}}">
<small
class="text-muted form-text">{{'userSeatsAdditionalDesc' | i18n : plans[plan].baseSeats : (plans[plan].seatPrice | currency:'$')}}</small>
class="text-muted form-text">{{'userSeatsAdditionalDesc' | i18n : selectedPlan.baseSeats : (seatPriceMonthly(selectedPlan) | currency:'$')}}</small>
</div>
</div>
<div class="row">
@@ -129,11 +118,11 @@
[(ngModel)]="additionalStorage" min="0" max="99" step="1"
placeholder="{{'additionalStorageGbDesc' | i18n}}">
<small
class="text-muted form-text">{{'additionalStorageIntervalDesc' | i18n : '1 GB' : (storageGb.price | currency:'$') : ('month' | i18n)}}</small>
class="text-muted form-text">{{'additionalStorageIntervalDesc' | i18n : '1 GB' : (additionalStoragePriceMonthly(selectedPlan) | currency:'$') : ('month' | i18n)}}</small>
</div>
</div>
<div class="row">
<div class="form-group col-6" *ngIf="plans[plan].canBuyPremiumAccessAddon">
<div class="form-group col-6" *ngIf="selectedPlan.hasPremiumAccessOption">
<div class="form-check">
<input id="premiumAccess" class="form-check-input" type="checkbox" name="PremiumAccessAddon"
[(ngModel)]="premiumAccessAddon">
@@ -144,62 +133,77 @@
</div>
</div>
<h2 class="spaced-header">{{'summary' | i18n}}</h2>
<div class="form-check form-check-block">
<input class="form-check-input" type="radio" name="BillingInterval" id="intervalAnnually" value="year"
[(ngModel)]="interval">
<label class="form-check-label" for="intervalAnnually">
{{'annually' | i18n}}
<small *ngIf="plans[plan].annualBasePrice">
{{'basePrice' | i18n}}: {{plans[plan].basePrice | currency:'$'}} &times;12 {{'monthAbbr' | i18n}} =
{{baseTotal(true) | currency:'$'}}
/{{'year' | i18n}}
</small>
<small *ngIf="!plans[plan].noAdditionalSeats">
<span *ngIf="plans[plan].baseSeats">{{'additionalUsers' | i18n}}:</span>
<span *ngIf="!plans[plan].baseSeats">{{'users' | i18n}}:</span>
{{additionalSeats || 0}} &times; {{plans[plan].seatPrice | currency:'$'}} &times;12
{{'monthAbbr' | i18n}} = {{seatTotal(true)
<div class="form-check form-check-block" *ngFor="let selectablePlan of selectablePlans">
<input class="form-check-input" type="radio" name="BillingInterval" id="interval{{selectablePlan.type}}"
[value]="selectablePlan.type" [(ngModel)]="plan">
<label class="form-check-label" for="interval{{selectablePlan.type}}">
<ng-container *ngIf="selectablePlan.isAnnual">
{{'annually' | i18n}}
<small *ngIf="selectablePlan.basePrice">
{{'basePrice' | i18n}}: {{ selectablePlan.basePrice / 12 | currency:'$'}} &times;12
{{'monthAbbr' | i18n}}
=
{{selectablePlan.basePrice | currency:'$'}}
/{{'year' | i18n}}
</small>
<small *ngIf="selectablePlan.hasAdditionalSeatsOption">
<span *ngIf="selectablePlan.baseSeats">{{'additionalUsers' | i18n}}:</span>
<span *ngIf="!selectablePlan.baseSeats">{{'users' | i18n}}:</span>
{{additionalSeats || 0}} &times; {{selectablePlan.seatPrice / 12 | currency:'$'}} &times;12
{{'monthAbbr' | i18n}} = {{seatTotal(selectablePlan)
| currency:'$'}} /{{'year' | i18n}}
</small>
<small>
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} &times;
{{storageGb.price | currency:'$'}} &times;12 {{'monthAbbr'
| i18n}} = {{additionalStorageTotal(true) | currency:'$'}} /{{'year' | i18n}}
</small>
<small *ngIf="plans[plan].canBuyPremiumAccessAddon && premiumAccessAddon">
{{'premiumAccess' | i18n}}:
{{3.33 | currency:'$'}} &times;12 {{'monthAbbr' | i18n}} = {{40 | currency:'$'}} /{{'year' | i18n}}
</small>
</label>
</div>
<div class="form-check form-check-block" *ngIf="plans[plan].monthlySeatPrice">
<input class="form-check-input" type="radio" name="BillingInterval" id="intervalMonthly" value="month"
[(ngModel)]="interval">
<label class="form-check-label" for="intervalMonthly">
{{'monthly' | i18n}}
<small *ngIf="plans[plan].monthlyBasePrice">
{{'basePrice' | i18n}}: {{baseTotal(false) | currency:'$'}} /{{'month' | i18n}}
</small>
<small *ngIf="!plans[plan].noAdditionalSeats">
<span *ngIf="plans[plan].baseSeats">{{'additionalUsers' | i18n}}:</span>
<span *ngIf="!plans[plan].baseSeats">{{'users' | i18n}}:</span>
{{additionalSeats || 0}} &times; {{plans[plan].monthlySeatPrice | currency:'$'}} =
{{seatTotal(false) | currency:'$'}} /{{'month'
| i18n}}
</small>
<small>
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} &times;
{{storageGb.monthlyPrice | currency:'$'}} = {{additionalStorageTotal(false)
</small>
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} &times;
{{selectablePlan.additionalStoragePricePerGb / 12 | currency:'$'}} &times;12 {{'monthAbbr'
| i18n}} = {{additionalStorageTotal(selectablePlan) | currency:'$'}}
/{{'year' | i18n}}
</small>
<small *ngIf="selectablePlan.hasPremiumAccessOption && premiumAccessAddon">
{{'premiumAccess' | i18n}}:
{{selectablePlan.premiumAccessOptionCost / 12 | currency:'$'}} &times;12 {{'monthAbbr' | i18n}}
=
{{40 | currency:'$'}}
/{{'year' | i18n}}
</small>
</ng-container>
<ng-container *ngIf="!selectablePlan.isAnnual">
{{'monthly' | i18n}}
<small *ngIf="selectablePlan.basePrice">
{{'basePrice' | i18n}}: {{selectablePlan.basePrice | currency:'$'}} {{'monthAbbr' | i18n}}
=
{{selectablePlan.basePrice | currency:'$'}}
/{{'month' | i18n}}
</small>
<small *ngIf="selectablePlan.hasAdditionalSeatsOption">
<span *ngIf="selectablePlan.baseSeats">{{'additionalUsers' | i18n}}:</span>
<span *ngIf="!selectablePlan.baseSeats">{{'users' | i18n}}:</span>
{{additionalSeats || 0}} &times; {{selectablePlan.seatPrice | currency:'$'}}
{{'monthAbbr' | i18n}} = {{seatTotal(selectablePlan)
| currency:'$'}} /{{'month' | i18n}}
</small>
</small>
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} &times;
{{selectablePlan.additionalStoragePricePerGb | currency:'$'}} {{'monthAbbr'
| i18n}} = {{additionalStorageTotal(selectablePlan) | currency:'$'}}
/{{'month' | i18n}}
</small>
<small *ngIf="selectablePlan.hasPremiumAccessOption && premiumAccessAddon">
{{'premiumAccess' | i18n}}:
{{selectablePlan.premiumAccessOptionCost | currency:'$'}} {{'monthAbbr' | i18n}} =
{{40 | currency:'$'}}
/{{'month' | i18n}}
</small>
</ng-container>
</label>
</div>
<hr class="my-3">
<div class="text-lg">
<strong>{{'total' | i18n}}:</strong> {{total | currency:'USD $'}} /{{interval | i18n}}
<strong>{{'total' | i18n}}:</strong> {{subtotal | currency:'USD $'}} /{{selectedPlanInterval | i18n}}
</div>
<ng-container *ngIf="createOrganization">
<small class="text-muted font-italic">{{'paymentChargedWithTrial' | i18n : (interval | i18n) }}</small>
<small
class="text-muted font-italic">{{'paymentChargedWithTrial' | i18n : (selectedPlanInterval | i18n) }}</small>
<h2 class="spaced-header mb-4">{{'paymentInformation' | i18n}}</h2>
<app-payment [hideCredit]="true"></app-payment>
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
@@ -209,8 +213,8 @@
</ng-container>
<small class="text-muted font-italic mt-2 d-block" *ngIf="!createOrganization">
{{'paymentCharged' | i18n : (interval | i18n) }}</small>
</ng-container>
<div [ngClass]="{'mt-4': !createOrganization || plans[plan].noPayment}">
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'submit' | i18n}}</span>