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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 280 additions and 291 deletions

View File

@ -24,7 +24,7 @@
</app-callout>
<dl *ngIf="selfHosted">
<dt>{{'billingPlan' | i18n}}</dt>
<dd>{{sub.plan}}</dd>
<dd>{{sub.plan.name}}</dd>
<dt>{{'expiration' | i18n}}</dt>
<dd *ngIf="sub.expiration">
{{sub.expiration | date:'mediumDate'}}
@ -39,7 +39,7 @@
<div class="col-4">
<dl>
<dt>{{'billingPlan' | i18n}}</dt>
<dd>{{sub.plan}}</dd>
<dd>{{sub.plan.name}}</dd>
<ng-container *ngIf="subscription">
<dt>{{'status' | i18n}}</dt>
<dd>

View File

@ -13,7 +13,6 @@ import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { TokenService } from 'jslib/abstractions/token.service';
import { PlanType } from 'jslib/enums/planType';
@ -38,10 +37,10 @@ export class OrganizationSubscriptionComponent implements OnInit {
cancelPromise: Promise<any>;
reinstatePromise: Promise<any>;
constructor(private tokenService: TokenService, private apiService: ApiService,
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private messagingService: MessagingService, private route: ActivatedRoute) {
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private analytics: Angulartics2,
private toasterService: ToasterService, private messagingService: MessagingService,
private route: ActivatedRoute) {
this.selfHosted = platformUtilsService.isSelfHost();
}
@ -192,34 +191,20 @@ export class OrganizationSubscriptionComponent implements OnInit {
}
get billingInterval() {
const monthly = this.sub.planType === PlanType.EnterpriseMonthly ||
this.sub.planType === PlanType.TeamsMonthly;
const monthly = !this.sub.plan.isAnnual;
return monthly ? 'month' : 'year';
}
get storageGbPrice() {
return this.billingInterval === 'month' ? 0.5 : 4;
return this.sub.plan.additionalStoragePricePerGb;
}
get seatPrice() {
switch (this.sub.planType) {
case PlanType.EnterpriseMonthly:
return 4;
case PlanType.EnterpriseAnnually:
return 36;
case PlanType.TeamsMonthly:
return 2.5;
case PlanType.TeamsAnnually:
return 24;
default:
return 0;
}
return this.sub.plan.seatPrice;
}
get canAdjustSeats() {
return this.sub.planType === PlanType.EnterpriseMonthly ||
this.sub.planType === PlanType.EnterpriseAnnually ||
this.sub.planType === PlanType.TeamsMonthly || this.sub.planType === PlanType.TeamsAnnually;
return this.sub.plan.hasAdditionalSeatsOption;
}
get canDownloadLicense() {

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>

View File

@ -2,6 +2,7 @@ import {
Component,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
} from '@angular/core';
@ -22,76 +23,39 @@ import { PaymentComponent } from './payment.component';
import { TaxInfoComponent } from './tax-info.component';
import { PlanType } from 'jslib/enums/planType';
import { ProductType } from 'jslib/enums/productType';
import { OrganizationCreateRequest } from 'jslib/models/request/organizationCreateRequest';
import { OrganizationUpgradeRequest } from 'jslib/models/request/organizationUpgradeRequest';
import { PlanResponse } from 'jslib/models/response/planResponse';
@Component({
selector: 'app-organization-plans',
templateUrl: 'organization-plans.component.html',
})
export class OrganizationPlansComponent {
export class OrganizationPlansComponent implements OnInit {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent) taxComponent: TaxInfoComponent;
@Input() organizationId: string;
@Input() showFree = true;
@Input() showCancel = false;
@Input() plan = 'free';
@Input() product: ProductType = ProductType.Free;
@Input() plan: PlanType = PlanType.Free;
@Output() onSuccess = new EventEmitter();
@Output() onCanceled = new EventEmitter();
selfHosted = false;
ownedBusiness = false;
premiumAccessAddon = false;
storageGbPriceMonthly = 0.33;
additionalStorage = 0;
additionalSeats = 0;
interval = 'year';
loading: boolean = true;
selfHosted: boolean = false;
ownedBusiness: boolean = false;
premiumAccessAddon: boolean = false;
additionalStorage: number = 0;
additionalSeats: number = 0;
name: string;
billingEmail: string;
businessName: string;
storageGb: any = {
price: 0.33,
monthlyPrice: 0.50,
yearlyPrice: 4,
};
plans: any = {
free: {
basePrice: 0,
noAdditionalSeats: true,
noPayment: true,
},
families: {
basePrice: 1,
annualBasePrice: 12,
baseSeats: 5,
noAdditionalSeats: true,
annualPlanType: PlanType.FamiliesAnnually,
canBuyPremiumAccessAddon: true,
},
teams: {
basePrice: 5,
annualBasePrice: 60,
monthlyBasePrice: 8,
baseSeats: 5,
seatPrice: 2,
annualSeatPrice: 24,
monthlySeatPrice: 2.5,
monthPlanType: PlanType.TeamsMonthly,
annualPlanType: PlanType.TeamsAnnually,
},
enterprise: {
seatPrice: 3,
annualSeatPrice: 36,
monthlySeatPrice: 4,
monthPlanType: PlanType.EnterpriseMonthly,
annualPlanType: PlanType.EnterpriseAnnually,
},
};
formPromise: Promise<any>;
plans: PlanResponse[];
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
@ -100,6 +64,134 @@ export class OrganizationPlansComponent {
this.selfHosted = platformUtilsService.isSelfHost();
}
async ngOnInit() {
const plans = await this.apiService.getPlans();
this.plans = plans.data;
this.loading = false;
}
get createOrganization() {
return this.organizationId == null;
}
get productTypes() {
return ProductType;
}
get selectedPlan() {
return this.plans.find((plan) => plan.type === this.plan);
}
get selectedPlanInterval() {
return this.selectedPlan.isAnnual
? 'year'
: 'month';
}
get selectableProducts() {
let validPlans = this.plans;
if (this.ownedBusiness) {
validPlans = validPlans.filter((plan) => plan.canBeUsedByBusiness);
}
if (!this.showFree) {
validPlans = validPlans.filter((plan) => plan.product !== ProductType.Free);
}
validPlans = validPlans
.filter((plan) => !plan.legacyYear
&& !plan.disabled
&& (plan.isAnnual || plan.product === this.productTypes.Free));
return validPlans;
}
get selectablePlans() {
return this.plans.filter((plan) => !plan.legacyYear && !plan.disabled && plan.product === this.product);
}
additionalStoragePriceMonthly(selectedPlan: PlanResponse) {
if (!selectedPlan.isAnnual) {
return selectedPlan.additionalStoragePricePerGb;
}
return selectedPlan.additionalStoragePricePerGb / 12;
}
seatPriceMonthly(selectedPlan: PlanResponse) {
if (!selectedPlan.isAnnual) {
return selectedPlan.seatPrice;
}
return selectedPlan.seatPrice / 12;
}
additionalStorageTotal(plan: PlanResponse): number {
if (!plan.hasAdditionalStorageOption) {
return 0;
}
return plan.additionalStoragePricePerGb * Math.abs(this.additionalStorage || 0);
}
seatTotal(plan: PlanResponse): number {
if (!plan.hasAdditionalSeatsOption) {
return 0;
}
return plan.seatPrice * Math.abs(this.additionalSeats || 0);
}
get subtotal() {
let subTotal = this.selectedPlan.basePrice;
if (this.selectedPlan.hasAdditionalSeatsOption && this.additionalSeats) {
subTotal += this.seatTotal(this.selectedPlan);
}
if (this.selectedPlan.hasAdditionalStorageOption && this.additionalStorage) {
subTotal += this.additionalStorageTotal(this.selectedPlan);
}
if (this.selectedPlan.hasPremiumAccessOption && this.premiumAccessAddon) {
subTotal += this.selectedPlan.premiumAccessOptionPrice;
}
return subTotal;
}
changedProduct() {
this.plan = this.selectablePlans[0].type;
if (!this.selectedPlan.hasPremiumAccessOption) {
this.premiumAccessAddon = false;
}
if (!this.selectedPlan.hasAdditionalStorageOption) {
this.additionalStorage = 0;
}
if (!this.selectedPlan.hasAdditionalSeatsOption) {
this.additionalSeats = 0;
} else if (!this.additionalSeats && !this.selectedPlan.baseSeats &&
this.selectedPlan.hasAdditionalSeatsOption) {
this.additionalSeats = 1;
}
}
changedOwnedBusiness() {
if (!this.ownedBusiness || this.selectedPlan.canBeUsedByBusiness) {
return;
}
this.plan = PlanType.TeamsMonthly;
}
changedCountry() {
this.paymentComponent.hideBank = this.taxComponent.taxInfo.country !== 'US';
// Bank Account payments are only available for US customers
if (this.paymentComponent.hideBank &&
this.paymentComponent.method === PaymentMethodType.BankAccount) {
this.paymentComponent.method = PaymentMethodType.Card;
this.paymentComponent.changeMethod();
}
}
cancel() {
this.onCanceled.emit();
}
async submit() {
let files: FileList = null;
if (this.createOrganization && this.selfHosted) {
@ -117,7 +209,7 @@ export class OrganizationPlansComponent {
let orgId: string = null;
if (this.createOrganization) {
let tokenResult: [string, PaymentMethodType] = null;
if (!this.selfHosted && this.plan !== 'free') {
if (!this.selfHosted && this.plan !== PlanType.Free) {
tokenResult = await this.paymentComponent.createPaymentToken();
}
const shareKey = await this.cryptoService.makeShareKey();
@ -140,7 +232,7 @@ export class OrganizationPlansComponent {
request.name = this.name;
request.billingEmail = this.billingEmail;
if (this.plan === 'free') {
if (this.selectedPlan.type === PlanType.Free) {
request.planType = PlanType.Free;
} else {
request.paymentToken = tokenResult[0];
@ -148,13 +240,9 @@ export class OrganizationPlansComponent {
request.businessName = this.ownedBusiness ? this.businessName : null;
request.additionalSeats = this.additionalSeats;
request.additionalStorageGb = this.additionalStorage;
request.premiumAccessAddon = this.plans[this.plan].canBuyPremiumAccessAddon &&
request.premiumAccessAddon = this.selectedPlan.hasPremiumAccessOption &&
this.premiumAccessAddon;
if (this.interval === 'month') {
request.planType = this.plans[this.plan].monthPlanType;
} else {
request.planType = this.plans[this.plan].annualPlanType;
}
request.planType = this.selectedPlan.type;
request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode;
request.billingAddressCountry = this.taxComponent.taxInfo.country;
if (this.taxComponent.taxInfo.includeTaxId) {
@ -173,13 +261,9 @@ export class OrganizationPlansComponent {
request.businessName = this.ownedBusiness ? this.businessName : null;
request.additionalSeats = this.additionalSeats;
request.additionalStorageGb = this.additionalStorage;
request.premiumAccessAddon = this.plans[this.plan].canBuyPremiumAccessAddon &&
request.premiumAccessAddon = this.selectedPlan.hasPremiumAccessOption &&
this.premiumAccessAddon;
if (this.interval === 'month') {
request.planType = this.plans[this.plan].monthPlanType;
} else {
request.planType = this.plans[this.plan].annualPlanType;
}
request.planType = this.selectedPlan.type;
const result = await this.apiService.postOrganizationUpgrade(this.organizationId, request);
if (!result.success && result.paymentIntentClientSecret != null) {
await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null);
@ -202,94 +286,10 @@ export class OrganizationPlansComponent {
}
};
this.formPromise = doSubmit();
await this.formPromise;
const formPromise = doSubmit();
await formPromise;
this.onSuccess.emit();
} catch { }
}
cancel() {
this.onCanceled.emit();
}
changedPlan() {
if (!this.plans[this.plan].canBuyPremiumAccessAddon) {
this.premiumAccessAddon = false;
}
if (this.plans[this.plan].monthPlanType == null) {
this.interval = 'year';
}
if (this.plans[this.plan].noAdditionalSeats) {
this.additionalSeats = 0;
} else if (!this.additionalSeats && !this.plans[this.plan].baseSeats &&
!this.plans[this.plan].noAdditionalSeats) {
this.additionalSeats = 1;
}
}
changedOwnedBusiness() {
if (!this.ownedBusiness || this.plan === 'teams' || this.plan === 'enterprise') {
return;
}
this.plan = 'teams';
}
additionalStorageTotal(annual: boolean): number {
if (annual) {
return Math.abs(this.additionalStorage || 0) * this.storageGb.yearlyPrice;
} else {
return Math.abs(this.additionalStorage || 0) * this.storageGb.monthlyPrice;
}
}
seatTotal(annual: boolean): number {
if (this.plans[this.plan].noAdditionalSeats) {
return 0;
}
if (annual) {
return this.plans[this.plan].annualSeatPrice * Math.abs(this.additionalSeats || 0);
} else {
return this.plans[this.plan].monthlySeatPrice * Math.abs(this.additionalSeats || 0);
}
}
baseTotal(annual: boolean): number {
if (annual) {
return Math.abs(this.plans[this.plan].annualBasePrice || 0);
} else {
return Math.abs(this.plans[this.plan].monthlyBasePrice || 0);
}
}
premiumAccessTotal(annual: boolean): number {
if (this.plans[this.plan].canBuyPremiumAccessAddon && this.premiumAccessAddon) {
if (annual) {
return 40;
}
}
return 0;
}
changedCountry() {
this.paymentComponent.hideBank = this.taxComponent.taxInfo.country !== 'US';
// Bank Account payments are only available for US customers
if (this.paymentComponent.hideBank &&
this.paymentComponent.method === PaymentMethodType.BankAccount) {
this.paymentComponent.method = PaymentMethodType.Card;
this.paymentComponent.changeMethod();
}
}
get total(): number {
const annual = this.interval === 'year';
return this.baseTotal(annual) + this.seatTotal(annual) + this.additionalStorageTotal(annual) +
this.premiumAccessTotal(annual);
}
get createOrganization() {
return this.organizationId == null;
}
}