diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 92e7930fb0f..63a4d71efea 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -106,6 +106,7 @@ import { DeauthorizeSessionsComponent } from './settings/deauthorize-sessions.co
import { DeleteAccountComponent } from './settings/delete-account.component';
import { DomainRulesComponent } from './settings/domain-rules.component';
import { OptionsComponent } from './settings/options.component';
+import { OrganizationPlansComponent } from './settings/organization-plans.component';
import { OrganizationsComponent } from './settings/organizations.component';
import { PaymentComponent } from './settings/payment.component';
import { PremiumComponent } from './settings/premium.component';
@@ -284,6 +285,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
OrgAddEditComponent,
OrgApiKeyComponent,
OrganizationBillingComponent,
+ OrganizationPlansComponent,
OrganizationSubscriptionComponent,
OrgAttachmentsComponent,
OrgCiphersComponent,
diff --git a/src/app/organizations/settings/change-plan.component.html b/src/app/organizations/settings/change-plan.component.html
index 8aef7f8186a..4b6b78a5256 100644
--- a/src/app/organizations/settings/change-plan.component.html
+++ b/src/app/organizations/settings/change-plan.component.html
@@ -1,14 +1,10 @@
-
+
diff --git a/src/app/settings/create-organization.component.ts b/src/app/settings/create-organization.component.ts
index f25662d370b..79d649dfbd9 100644
--- a/src/app/settings/create-organization.component.ts
+++ b/src/app/settings/create-organization.component.ts
@@ -3,243 +3,27 @@ import {
OnInit,
ViewChild,
} from '@angular/core';
-import {
- ActivatedRoute,
- Router,
-} from '@angular/router';
+import { ActivatedRoute } from '@angular/router';
-import { ToasterService } from 'angular2-toaster';
-import { Angulartics2 } from 'angulartics2';
-
-import { ApiService } from 'jslib/abstractions/api.service';
-import { CryptoService } from 'jslib/abstractions/crypto.service';
-import { I18nService } from 'jslib/abstractions/i18n.service';
-import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
-import { SyncService } from 'jslib/abstractions/sync.service';
-
-import { PaymentComponent } from './payment.component';
-
-import { PlanType } from 'jslib/enums/planType';
-import { OrganizationCreateRequest } from 'jslib/models/request/organizationCreateRequest';
+import { OrganizationPlansComponent } from './organization-plans.component';
@Component({
selector: 'app-create-organization',
templateUrl: 'create-organization.component.html',
})
export class CreateOrganizationComponent implements OnInit {
- @ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
+ @ViewChild(OrganizationPlansComponent) orgPlansComponent: OrganizationPlansComponent;
- selfHosted = false;
- ownedBusiness = false;
- premiumAccessAddon = false;
- storageGbPriceMonthly = 0.33;
- additionalStorage = 0;
- additionalSeats = 0;
- plan = 'free';
- interval = 'year';
- 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;
-
- constructor(private apiService: ApiService, private i18nService: I18nService,
- private analytics: Angulartics2, private toasterService: ToasterService,
- platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService,
- private router: Router, private syncService: SyncService,
- private route: ActivatedRoute) {
- this.selfHosted = platformUtilsService.isSelfHost();
- }
+ constructor(private route: ActivatedRoute) { }
ngOnInit() {
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
if (qParams.plan === 'families' || qParams.plan === 'teams' || qParams.plan === 'enterprise') {
- this.plan = qParams.plan;
+ this.orgPlansComponent.plan = qParams.plan;
}
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
});
}
-
- async submit() {
- let files: FileList = null;
- if (this.selfHosted) {
- const fileEl = document.getElementById('file') as HTMLInputElement;
- files = fileEl.files;
- if (files == null || files.length === 0) {
- this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
- this.i18nService.t('selectFile'));
- return;
- }
- }
-
- let key: string = null;
- let collectionCt: string = null;
-
- try {
- this.formPromise = this.cryptoService.makeShareKey().then((shareKey) => {
- key = shareKey[0].encryptedString;
- return this.cryptoService.encrypt(this.i18nService.t('defaultCollection'), shareKey[1]);
- }).then((collection) => {
- collectionCt = collection.encryptedString;
- if (this.selfHosted || this.plan === 'free') {
- return null;
- } else {
- return this.paymentComponent.createPaymentToken();
- }
- }).then((tokenResult) => {
- if (this.selfHosted) {
- const fd = new FormData();
- fd.append('license', files[0]);
- fd.append('key', key);
- fd.append('collectionName', collectionCt);
- return this.apiService.postOrganizationLicense(fd);
- } else {
- const request = new OrganizationCreateRequest();
- request.key = key;
- request.collectionName = collectionCt;
- request.name = this.name;
- request.billingEmail = this.billingEmail;
-
- if (this.plan === 'free') {
- request.planType = PlanType.Free;
- } else {
- request.paymentToken = tokenResult[0];
- request.paymentMethodType = tokenResult[1];
- request.businessName = this.ownedBusiness ? this.businessName : null;
- request.additionalSeats = this.additionalSeats;
- request.additionalStorageGb = this.additionalStorage;
- request.premiumAccessAddon = this.plans[this.plan].canBuyPremiumAccessAddon &&
- this.premiumAccessAddon;
- if (this.interval === 'month') {
- request.planType = this.plans[this.plan].monthPlanType;
- } else {
- request.planType = this.plans[this.plan].annualPlanType;
- }
- }
- return this.apiService.postOrganization(request);
- }
- }).then((response) => {
- return this.finalize(response.id);
- });
- await this.formPromise;
- } catch { }
- }
-
- async finalize(orgId: string) {
- await this.apiService.refreshIdentityToken();
- await this.syncService.fullSync(true);
- this.analytics.eventTrack.next({ action: 'Created Organization' });
- this.toasterService.popAsync('success', this.i18nService.t('organizationCreated'),
- this.i18nService.t('organizationReadyToGo'));
- this.router.navigate(['/organizations/' + orgId]);
- }
-
- 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 (this.additionalStorage || 0) * this.storageGb.yearlyPrice;
- } else {
- return (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 * (this.additionalSeats || 0);
- } else {
- return this.plans[this.plan].monthlySeatPrice * (this.additionalSeats || 0);
- }
- }
-
- baseTotal(annual: boolean): number {
- if (annual) {
- return (this.plans[this.plan].annualBasePrice || 0);
- } else {
- return (this.plans[this.plan].monthlyBasePrice || 0);
- }
- }
-
- premiumAccessTotal(annual: boolean): number {
- if (this.plans[this.plan].canBuyPremiumAccessAddon && this.premiumAccessAddon) {
- if (annual) {
- return 40;
- }
- }
- return 0;
- }
-
- get total(): number {
- const annual = this.interval === 'year';
- return this.baseTotal(annual) + this.seatTotal(annual) + this.additionalStorageTotal(annual) +
- this.premiumAccessTotal(annual);
- }
}
diff --git a/src/app/settings/organization-plans.component.html b/src/app/settings/organization-plans.component.html
new file mode 100644
index 00000000000..e45f4b0d980
--- /dev/null
+++ b/src/app/settings/organization-plans.component.html
@@ -0,0 +1,215 @@
+
+ {{'uploadLicenseFileOrg' | i18n}}
+
+
+
diff --git a/src/app/settings/organization-plans.component.ts b/src/app/settings/organization-plans.component.ts
new file mode 100644
index 00000000000..8da6ce3d804
--- /dev/null
+++ b/src/app/settings/organization-plans.component.ts
@@ -0,0 +1,257 @@
+import {
+ Component,
+ EventEmitter,
+ Input,
+ Output,
+ ViewChild,
+} from '@angular/core';
+import { Router } from '@angular/router';
+
+import { ToasterService } from 'angular2-toaster';
+import { Angulartics2 } from 'angulartics2';
+
+import { PaymentMethodType } from 'jslib/enums/paymentMethodType';
+
+import { ApiService } from 'jslib/abstractions/api.service';
+import { CryptoService } from 'jslib/abstractions/crypto.service';
+import { I18nService } from 'jslib/abstractions/i18n.service';
+import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
+import { SyncService } from 'jslib/abstractions/sync.service';
+
+import { PaymentComponent } from './payment.component';
+
+import { PlanType } from 'jslib/enums/planType';
+import { OrganizationCreateRequest } from 'jslib/models/request/organizationCreateRequest';
+
+@Component({
+ selector: 'app-organization-plans',
+ templateUrl: 'organization-plans.component.html',
+})
+export class OrganizationPlansComponent {
+ @ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
+
+ @Input() organizationId: string;
+ @Input() showFree = true;
+ @Input() showCancel = false;
+ @Input() plan = 'free';
+ @Output() onSuccess = new EventEmitter();
+ @Output() onCanceled = new EventEmitter();
+
+ selfHosted = false;
+ ownedBusiness = false;
+ premiumAccessAddon = false;
+ storageGbPriceMonthly = 0.33;
+ additionalStorage = 0;
+ additionalSeats = 0;
+ interval = 'year';
+ 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;
+
+ constructor(private apiService: ApiService, private i18nService: I18nService,
+ private analytics: Angulartics2, private toasterService: ToasterService,
+ platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService,
+ private router: Router, private syncService: SyncService) {
+ this.selfHosted = platformUtilsService.isSelfHost();
+ }
+
+ async submit() {
+ let files: FileList = null;
+ if (this.createOrganization && this.selfHosted) {
+ const fileEl = document.getElementById('file') as HTMLInputElement;
+ files = fileEl.files;
+ if (files == null || files.length === 0) {
+ this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
+ this.i18nService.t('selectFile'));
+ return;
+ }
+ }
+
+ try {
+ this.formPromise = this.doSubmit(files);
+ await this.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;
+ }
+
+ 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;
+ }
+
+ private async doSubmit(files: FileList) {
+ let tokenResult: [string, PaymentMethodType] = null;
+ if (!this.selfHosted && this.plan !== 'free') {
+ tokenResult = await this.paymentComponent.createPaymentToken();
+ }
+
+ let orgId: string = null;
+ if (this.createOrganization) {
+ const shareKey = await this.cryptoService.makeShareKey();
+ const key = shareKey[0].encryptedString;
+ const collection = await this.cryptoService.encrypt(this.i18nService.t('defaultCollection'), shareKey[1]);
+ const collectionCt = collection.encryptedString;
+
+ if (this.selfHosted) {
+ const fd = new FormData();
+ fd.append('license', files[0]);
+ fd.append('key', key);
+ fd.append('collectionName', collectionCt);
+ const response = await this.apiService.postOrganizationLicense(fd);
+ orgId = response.id;
+ } else {
+ const request = new OrganizationCreateRequest();
+ request.key = key;
+ request.collectionName = collectionCt;
+ request.name = this.name;
+ request.billingEmail = this.billingEmail;
+
+ if (this.plan === 'free') {
+ request.planType = PlanType.Free;
+ } else {
+ request.paymentToken = tokenResult[0];
+ request.paymentMethodType = tokenResult[1];
+ request.businessName = this.ownedBusiness ? this.businessName : null;
+ request.additionalSeats = this.additionalSeats;
+ request.additionalStorageGb = this.additionalStorage;
+ request.premiumAccessAddon = this.plans[this.plan].canBuyPremiumAccessAddon &&
+ this.premiumAccessAddon;
+ if (this.interval === 'month') {
+ request.planType = this.plans[this.plan].monthPlanType;
+ } else {
+ request.planType = this.plans[this.plan].annualPlanType;
+ }
+ }
+ const response = await this.apiService.postOrganization(request);
+ orgId = response.id;
+ }
+ } else {
+ // TODO
+ orgId = this.organizationId;
+ }
+
+ if (orgId != null) {
+ await this.apiService.refreshIdentityToken();
+ await this.syncService.fullSync(true);
+ this.analytics.eventTrack.next({
+ action: (this.createOrganization ? 'Created' : 'Upgraded') + ' Organization',
+ });
+ this.toasterService.popAsync('success',
+ this.i18nService.t(this.createOrganization ? 'organizationCreated' : ''), // TODO
+ this.i18nService.t('organizationReadyToGo'));
+ this.router.navigate(['/organizations/' + orgId]);
+ }
+ }
+}
diff --git a/src/app/settings/premium.component.ts b/src/app/settings/premium.component.ts
index 9db76776948..310d937c187 100644
--- a/src/app/settings/premium.component.ts
+++ b/src/app/settings/premium.component.ts
@@ -102,7 +102,7 @@ export class PremiumComponent implements OnInit {
}
get additionalStorageTotal(): number {
- return this.storageGbPrice * this.additionalStorage;
+ return this.storageGbPrice * Math.abs(this.additionalStorage || 0);
}
get total(): number {
diff --git a/src/scss/styles.scss b/src/scss/styles.scss
index 70cb5b3fc13..65b18595b3c 100644
--- a/src/scss/styles.scss
+++ b/src/scss/styles.scss
@@ -28,7 +28,7 @@ $h5-font-size: 1rem;
$h6-font-size: 1rem;
$small-font-size: 90%;
-$font-size-lg: 1.18rem;
+$font-size-lg: 1.15rem;
$code-font-size: 100%;
$navbar-padding-y: .75rem;
@@ -185,7 +185,7 @@ input, select, textarea {
}
.card-body-header {
- font-size: $h3-font-size * 1.12;
+ font-size: $font-size-lg;
@extend .mb-4
}
@@ -216,6 +216,12 @@ input, select, textarea {
}
}
+.card-org-plans {
+ h2 {
+ font-size: $font-size-lg;
+ }
+}
+
.modal-dialog {
width: $modal-md;
}