1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-06 10:33: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

@@ -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;
}
}