diff --git a/jslib b/jslib
index b2c700ad28..6b4ae1b8d5 160000
--- a/jslib
+++ b/jslib
@@ -1 +1 @@
-Subproject commit b2c700ad285d9336426284f766b434be8c509f0a
+Subproject commit 6b4ae1b8d591e973fe4036d3c10b6f0be81debc9
diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index 7679bbce34..7872bb4023 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -26,6 +26,7 @@ import { ManageComponent as OrgManageComponent } from './organizations/manage/ma
import { PeopleComponent as OrgPeopleComponent } from './organizations/manage/people.component';
import { AccountComponent as OrgAccountComponent } from './organizations/settings/account.component';
+import { OrganizationBillingComponent } from './organizations/settings/organization-billing.component';
import { SettingsComponent as OrgSettingsComponent } from './organizations/settings/settings.component';
import { ExportComponent as OrgExportComponent } from './organizations/tools/export.component';
@@ -187,6 +188,11 @@ const routes: Routes = [
children: [
{ path: '', pathMatch: 'full', redirectTo: 'account' },
{ path: 'account', component: OrgAccountComponent, data: { titleId: 'myOrganization' } },
+ {
+ path: 'billing',
+ component: OrganizationBillingComponent,
+ data: { titleId: 'billingAndLicensing' },
+ },
],
},
],
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index f440021c9f..a1ebf397f5 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -53,6 +53,7 @@ import { UserGroupsComponent as OrgUserGroupsComponent } from './organizations/m
import { AccountComponent as OrgAccountComponent } from './organizations/settings/account.component';
import { DeleteOrganizationComponent } from './organizations/settings/delete-organization.component';
+import { OrganizationBillingComponent } from './organizations/settings/organization-billing.component';
import { SettingsComponent as OrgSettingComponent } from './organizations/settings/settings.component';
import { ExportComponent as OrgExportComponent } from './organizations/tools/export.component';
@@ -188,6 +189,7 @@ import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
OptionsComponent,
OrgAccountComponent,
OrgAddEditComponent,
+ OrganizationBillingComponent,
OrgAttachmentsComponent,
OrgCiphersComponent,
OrgCollectionAddEditComponent,
diff --git a/src/app/organizations/settings/delete-organization.component.ts b/src/app/organizations/settings/delete-organization.component.ts
index 148999cd4d..b8a584c112 100644
--- a/src/app/organizations/settings/delete-organization.component.ts
+++ b/src/app/organizations/settings/delete-organization.component.ts
@@ -34,7 +34,7 @@ export class DeleteOrganizationComponent {
const request = new PasswordVerificationRequest();
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
try {
- this.formPromise = this.apiService.postDeleteOrganization(this.organizationId, request);
+ this.formPromise = this.apiService.deleteOrganization(this.organizationId, request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Deleted Organization' });
this.toasterService.popAsync('success', this.i18nService.t('organizationDeleted'),
diff --git a/src/app/organizations/settings/organization-billing.component.html b/src/app/organizations/settings/organization-billing.component.html
new file mode 100644
index 0000000000..cc6d7324c4
--- /dev/null
+++ b/src/app/organizations/settings/organization-billing.component.html
@@ -0,0 +1,152 @@
+
+
+
+ {{'subscriptionCanceled' | i18n}}
+
+ {{'subscriptionPendingCanceled' | i18n}}
+
+
+
+ - {{'billingPlan' | i18n}}
+ - {{billing.plan}}
+ - {{'expiration' | i18n}}
+ - {{billing.expiration | date:'mediumDate'}}
+ - {{'neverExpires' | i18n}}
+
+
+
+
+ - {{'billingPlan' | i18n}}
+ - {{billing.plan}}
+
+ - {{'status' | i18n}}
+ -
+ {{subscription.status || '-'}}
+ {{'pendingCancellation' | i18n}}
+
+ - {{'nextCharge' | i18n}}
+ - {{nextInvoice ? ((nextInvoice.date | date: 'mediumDate') + ', ' + (nextInvoice.amount | currency:'$'))
+ : '-'}}
+
+
+
+
+
+
{{'details' | i18n}}
+
+
+
+
+ {{i.name}} {{i.quantity > 1 ? '×' + i.quantity : ''}} @ {{i.amount | currency:'$'}}
+ |
+
+ {{(i.quantity * i.amount) | currency:'$'}} /{{i.interval | i18n}}
+ |
+
+
+
+
+
+
+
+
+
+
{{'updateLicense' | i18n}}
+
+
+
+
+
+
+
+
+
+
+
+ {{'subscriptionStorage' | i18n : billing.maxStorageGb || 0 : billing.storageName || '0 MB'}}
+
+
{{(storagePercentage / 100) | percent}}
+
+
+
+
+
+
+
+
+
+
+
+ {{'noPaymentMethod' | i18n}}
+
+
+ {{'verifyBankAccountDesc' | i18n}} {{'verifyBankAccountFailureWarning' | i18n}}
+
+
+
+
+ {{paymentSource.description}}
+
+
+
+
+
+
+ {{'noCharges' | i18n}}
+
+
+
+
+
+
+
+ |
+ {{c.createdDate | date:'mediumDate'}} |
+ {{c.paymentSource ? c.paymentSource.description : '-'}} |
+ {{c.status}} |
+ {{c.amount | currency:'$'}} |
+
+
+
+ * {{'chargesStatement' | i18n : 'BITWARDEN'}}
+
+
diff --git a/src/app/organizations/settings/organization-billing.component.ts b/src/app/organizations/settings/organization-billing.component.ts
new file mode 100644
index 0000000000..cfcf200644
--- /dev/null
+++ b/src/app/organizations/settings/organization-billing.component.ts
@@ -0,0 +1,200 @@
+import {
+ Component,
+ OnInit,
+} from '@angular/core';
+import {
+ ActivatedRoute,
+ Router
+} from '@angular/router';
+
+import { ToasterService } from 'angular2-toaster';
+import { Angulartics2 } from 'angulartics2';
+
+
+import { BillingChargeResponse } from 'jslib/models/response/billingResponse';
+import { OrganizationBillingResponse } from 'jslib/models/response/organizationBillingResponse';
+
+import { ApiService } from 'jslib/abstractions/api.service';
+import { I18nService } from 'jslib/abstractions/i18n.service';
+import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
+import { TokenService } from 'jslib/abstractions/token.service';
+
+import { PaymentMethodType } from 'jslib/enums/paymentMethodType';
+import { PlanType } from 'jslib/enums/planType';
+
+@Component({
+ selector: 'app-org-billing',
+ templateUrl: 'organization-billing.component.html',
+})
+export class OrganizationBillingComponent implements OnInit {
+ loading = false;
+ firstLoaded = false;
+ organizationId: string;
+ adjustStorageAdd = true;
+ showAdjustStorage = false;
+ showAdjustPayment = false;
+ showUpdateLicense = false;
+ billing: OrganizationBillingResponse;
+ paymentMethodType = PaymentMethodType;
+ selfHosted = false;
+
+ cancelPromise: Promise;
+ reinstatePromise: Promise;
+
+ constructor(private tokenService: TokenService, private apiService: ApiService,
+ private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
+ private analytics: Angulartics2, private toasterService: ToasterService,
+ private router: Router, private route: ActivatedRoute) {
+ this.selfHosted = platformUtilsService.isSelfHost();
+ }
+
+ async ngOnInit() {
+ this.route.parent.parent.params.subscribe(async (params) => {
+ this.organizationId = params.organizationId;
+ await this.load();
+ this.firstLoaded = true;
+ });
+ }
+
+ async load() {
+ if (this.loading) {
+ return;
+ }
+ this.loading = true;
+ this.billing = await this.apiService.getOrganizationBilling(this.organizationId);
+ this.loading = false;
+ }
+
+ async reinstate() {
+ if (this.loading) {
+ return;
+ }
+
+ const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('reinstateConfirmation'),
+ this.i18nService.t('reinstateSubscription'), this.i18nService.t('yes'), this.i18nService.t('cancel'));
+ if (!confirmed) {
+ return;
+ }
+
+ try {
+ this.reinstatePromise = this.apiService.postReinstatePremium();
+ await this.reinstatePromise;
+ this.analytics.eventTrack.next({ action: 'Reinstated Premium' });
+ this.toasterService.popAsync('success', null, this.i18nService.t('reinstated'));
+ this.load();
+ } catch { }
+ }
+
+ async cancel() {
+ if (this.loading) {
+ return;
+ }
+
+ const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('cancelConfirmation'),
+ this.i18nService.t('cancelSubscription'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
+ if (!confirmed) {
+ return;
+ }
+
+ try {
+ this.cancelPromise = this.apiService.postCancelPremium();
+ await this.cancelPromise;
+ this.analytics.eventTrack.next({ action: 'Canceled Premium' });
+ this.toasterService.popAsync('success', null, this.i18nService.t('canceledSubscription'));
+ this.load();
+ } catch { }
+ }
+
+ downloadLicense() {
+ if (this.loading) {
+ return;
+ }
+ }
+
+ updateLicense() {
+ if (this.loading) {
+ return;
+ }
+ this.showUpdateLicense = true;
+ }
+
+ closeUpdateLicense(load: boolean) {
+ this.showUpdateLicense = false;
+ if (load) {
+ this.load();
+ }
+ }
+
+ adjustStorage(add: boolean) {
+ this.adjustStorageAdd = add;
+ this.showAdjustStorage = true;
+ }
+
+ closeStorage(load: boolean) {
+ this.showAdjustStorage = false;
+ if (load) {
+ this.load();
+ }
+ }
+
+ changePayment() {
+ this.showAdjustPayment = true;
+ }
+
+ closePayment(load: boolean) {
+ this.showAdjustPayment = false;
+ if (load) {
+ this.load();
+ }
+ }
+
+ async viewInvoice(charge: BillingChargeResponse) {
+ const token = await this.tokenService.getToken();
+ const url = this.apiService.apiBaseUrl + '/organizations/' + this.organizationId +
+ '/billing-invoice/' + charge.invoiceId + '?access_token=' + token;
+ this.platformUtilsService.launchUri(url);
+ }
+
+ get subscriptionMarkedForCancel() {
+ return this.subscription != null && !this.subscription.cancelled && this.subscription.cancelAtEndDate;
+ }
+
+ get subscription() {
+ return this.billing != null ? this.billing.subscription : null;
+ }
+
+ get nextInvoice() {
+ return this.billing != null ? this.billing.upcomingInvoice : null;
+ }
+
+ get paymentSource() {
+ return this.billing != null ? this.billing.paymentSource : null;
+ }
+
+ get charges() {
+ return this.billing != null ? this.billing.charges : null;
+ }
+
+ get storagePercentage() {
+ return this.billing != null && this.billing.maxStorageGb ?
+ +(100 * (this.billing.storageGb / this.billing.maxStorageGb)).toFixed(2) : 0;
+ }
+
+ get storageProgressWidth() {
+ return this.storagePercentage < 5 ? 5 : 0;
+ }
+
+ get billingInterval() {
+ const monthly = this.billing.planType === PlanType.EnterpriseMonthly ||
+ this.billing.planType === PlanType.TeamsMonthly;
+ return monthly ? 'month' : 'year';
+ }
+
+ get storageGbPrice() {
+ return this.billingInterval === 'month' ? 0.5 : 4;
+ }
+
+ get seatPrice() {
+ return 4;
+ }
+}
diff --git a/src/app/settings/adjust-payment.component.html b/src/app/settings/adjust-payment.component.html
index 03c45e7836..056820b246 100644
--- a/src/app/settings/adjust-payment.component.html
+++ b/src/app/settings/adjust-payment.component.html
@@ -10,7 +10,7 @@