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:
parent
c46af91240
commit
5f04950358
|
@ -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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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:'$'}} ×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}} × {{plans[plan].seatPrice | currency:'$'}} ×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:'$'}} ×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}} × {{selectablePlan.seatPrice / 12 | currency:'$'}} ×12
|
||||
{{'monthAbbr' | i18n}} = {{seatTotal(selectablePlan)
|
||||
| currency:'$'}} /{{'year' | i18n}}
|
||||
</small>
|
||||
<small>
|
||||
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} ×
|
||||
{{storageGb.price | currency:'$'}} ×12 {{'monthAbbr'
|
||||
| i18n}} = {{additionalStorageTotal(true) | currency:'$'}} /{{'year' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="plans[plan].canBuyPremiumAccessAddon && premiumAccessAddon">
|
||||
{{'premiumAccess' | i18n}}:
|
||||
{{3.33 | currency:'$'}} ×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}} × {{plans[plan].monthlySeatPrice | currency:'$'}} =
|
||||
{{seatTotal(false) | currency:'$'}} /{{'month'
|
||||
| i18n}}
|
||||
</small>
|
||||
<small>
|
||||
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} ×
|
||||
{{storageGb.monthlyPrice | currency:'$'}} = {{additionalStorageTotal(false)
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
|
||||
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} ×
|
||||
{{selectablePlan.additionalStoragePricePerGb / 12 | currency:'$'}} ×12 {{'monthAbbr'
|
||||
| i18n}} = {{additionalStorageTotal(selectablePlan) | currency:'$'}}
|
||||
/{{'year' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasPremiumAccessOption && premiumAccessAddon">
|
||||
{{'premiumAccess' | i18n}}:
|
||||
{{selectablePlan.premiumAccessOptionCost / 12 | currency:'$'}} ×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}} × {{selectablePlan.seatPrice | currency:'$'}}
|
||||
{{'monthAbbr' | i18n}} = {{seatTotal(selectablePlan)
|
||||
| currency:'$'}} /{{'month' | i18n}}
|
||||
</small>
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
|
||||
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} ×
|
||||
{{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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue