diff --git a/jslib b/jslib index 3e996ae9ad..8b411de034 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit 3e996ae9adf309f3ee4398630299fb0ab5312f9c +Subproject commit 8b411de034569b36788239a5948d64f5029d6437 diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index f7498200fd..6a4d436550 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -27,6 +27,7 @@ import { PeopleComponent as OrgPeopleComponent } from './organizations/manage/pe import { AccountComponent as OrgAccountComponent } from './organizations/settings/account.component'; import { OrganizationBillingComponent } from './organizations/settings/organization-billing.component'; +import { OrganizationSubscriptionComponent } from './organizations/settings/organization-subscription.component'; import { SettingsComponent as OrgSettingsComponent } from './organizations/settings/settings.component'; import { TwoFactorSetupComponent as OrgTwoFactorSetupComponent, @@ -62,6 +63,7 @@ import { PremiumComponent } from './settings/premium.component'; import { SettingsComponent } from './settings/settings.component'; import { TwoFactorSetupComponent } from './settings/two-factor-setup.component'; import { UserBillingComponent } from './settings/user-billing.component'; +import { UserSubscriptionComponent } from './settings/user-subscription.component'; import { BreachReportComponent } from './tools/breach-report.component'; import { ExportComponent } from './tools/export.component'; @@ -145,7 +147,12 @@ const routes: Routes = [ { path: 'domain-rules', component: DomainRulesComponent, data: { titleId: 'domainRules' } }, { path: 'two-factor', component: TwoFactorSetupComponent, data: { titleId: 'twoStepLogin' } }, { path: 'premium', component: PremiumComponent, data: { titleId: 'goPremium' } }, - { path: 'billing', component: UserBillingComponent, data: { titleId: 'billingAndLicensing' } }, + { path: 'billing', component: UserBillingComponent, data: { titleId: 'billing' } }, + { + path: 'subscription', + component: UserSubscriptionComponent, + data: { titleId: 'premiumMembership' }, + }, { path: 'organizations', component: OrganizationsComponent, data: { titleId: 'organizations' } }, { path: 'create-organization', @@ -271,7 +278,12 @@ const routes: Routes = [ { path: 'billing', component: OrganizationBillingComponent, - data: { titleId: 'billingAndLicensing' }, + data: { titleId: 'billing' }, + }, + { + path: 'subscription', + component: OrganizationSubscriptionComponent, + data: { titleId: 'subscription' }, }, ], }, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6013b3827d..fb1405cb23 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -57,6 +57,7 @@ import { AccountComponent as OrgAccountComponent } from './organizations/setting import { AdjustSeatsComponent } from './organizations/settings/adjust-seats.component'; import { DeleteOrganizationComponent } from './organizations/settings/delete-organization.component'; import { OrganizationBillingComponent } from './organizations/settings/organization-billing.component'; +import { OrganizationSubscriptionComponent } from './organizations/settings/organization-subscription.component'; import { SettingsComponent as OrgSettingComponent } from './organizations/settings/settings.component'; import { TwoFactorSetupComponent as OrgTwoFactorSetupComponent, @@ -116,6 +117,7 @@ import { TwoFactorYubiKeyComponent } from './settings/two-factor-yubikey.compone import { UpdateKeyComponent } from './settings/update-key.component'; import { UpdateLicenseComponent } from './settings/update-license.component'; import { UserBillingComponent } from './settings/user-billing.component'; +import { UserSubscriptionComponent } from './settings/user-subscription.component'; import { VerifyEmailComponent } from './settings/verify-email.component'; import { BreachReportComponent } from './tools/breach-report.component'; @@ -271,6 +273,7 @@ registerLocaleData(localeZhTw, 'zh-TW'); OrgAccountComponent, OrgAddEditComponent, OrganizationBillingComponent, + OrganizationSubscriptionComponent, OrgAttachmentsComponent, OrgCiphersComponent, OrgCollectionAddEditComponent, @@ -334,6 +337,7 @@ registerLocaleData(localeZhTw, 'zh-TW'); UpdateLicenseComponent, UserBillingComponent, UserLayoutComponent, + UserSubscriptionComponent, VaultComponent, VerifyEmailComponent, VerifyEmailTokenComponent, diff --git a/src/app/organizations/settings/organization-billing.component.ts b/src/app/organizations/settings/organization-billing.component.ts index b6874f05dd..3de11ce579 100644 --- a/src/app/organizations/settings/organization-billing.component.ts +++ b/src/app/organizations/settings/organization-billing.component.ts @@ -7,52 +7,20 @@ import { ActivatedRoute } from '@angular/router'; import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; -import { VerifyBankRequest } from 'jslib/models/request/verifyBankRequest'; - -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 { MessagingService } from 'jslib/abstractions/messaging.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'; -import { TransactionType } from 'jslib/enums/transactionType'; +import { UserBillingComponent } from '../../settings/user-billing.component'; @Component({ selector: 'app-org-billing', - templateUrl: 'organization-billing.component.html', + templateUrl: '../../settings/user-billing.component.html', }) -export class OrganizationBillingComponent implements OnInit { - loading = false; - firstLoaded = false; - organizationId: string; - adjustSeatsAdd = true; - showAdjustSeats = false; - adjustStorageAdd = true; - showAdjustStorage = false; - showAdjustPayment = false; - showUpdateLicense = false; - billing: OrganizationBillingResponse; - paymentMethodType = PaymentMethodType; - transactionType = TransactionType; - selfHosted = false; - verifyAmount1: number; - verifyAmount2: number; - - cancelPromise: Promise; - reinstatePromise: Promise; - licensePromise: Promise; - verifyBankPromise: Promise; - - 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) { - this.selfHosted = platformUtilsService.isSelfHost(); +export class OrganizationBillingComponent extends UserBillingComponent implements OnInit { + constructor(apiService: ApiService, i18nService: I18nService, + analytics: Angulartics2, toasterService: ToasterService, + private route: ActivatedRoute) { + super(apiService, i18nService, analytics, toasterService); } async ngOnInit() { @@ -62,227 +30,4 @@ export class OrganizationBillingComponent implements OnInit { 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.postOrganizationReinstate(this.organizationId); - await this.reinstatePromise; - this.analytics.eventTrack.next({ action: 'Reinstated Plan' }); - 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.postOrganizationCancel(this.organizationId); - await this.cancelPromise; - this.analytics.eventTrack.next({ action: 'Canceled Plan' }); - this.toasterService.popAsync('success', null, this.i18nService.t('canceledSubscription')); - this.load(); - } catch { } - } - - async changePlan() { - const contactSupport = await this.platformUtilsService.showDialog(this.i18nService.t('changeBillingPlanDesc'), - this.i18nService.t('changeBillingPlan'), this.i18nService.t('contactSupport'), this.i18nService.t('close')); - if (contactSupport) { - this.platformUtilsService.launchUri('https://bitwarden.com/contact'); - } - } - - async downloadLicense() { - if (this.loading) { - return; - } - - const installationId = window.prompt(this.i18nService.t('enterInstallationId')); - if (installationId == null || installationId === '') { - return; - } - - try { - this.licensePromise = this.apiService.getOrganizationLicense(this.organizationId, installationId); - const license = await this.licensePromise; - const licenseString = JSON.stringify(license, null, 2); - this.platformUtilsService.saveFile(window, licenseString, null, 'bitwarden_organization_license.json'); - } catch { } - } - - updateLicense() { - if (this.loading) { - return; - } - this.showUpdateLicense = true; - } - - async verifyBank() { - if (this.loading) { - return; - } - - try { - const request = new VerifyBankRequest(); - request.amount1 = this.verifyAmount1; - request.amount2 = this.verifyAmount2; - this.verifyBankPromise = this.apiService.postOrganizationVerifyBank(this.organizationId, request); - await this.verifyBankPromise; - this.analytics.eventTrack.next({ action: 'Verified Bank Account' }); - this.toasterService.popAsync('success', null, this.i18nService.t('verifiedBankAccount')); - this.load(); - } catch { } - } - - closeUpdateLicense(updated: boolean) { - this.showUpdateLicense = false; - if (updated) { - this.load(); - this.messagingService.send('updatedOrgLicense'); - } - } - - adjustSeats(add: boolean) { - this.adjustSeatsAdd = add; - this.showAdjustSeats = true; - } - - closeSeats(load: boolean) { - this.showAdjustSeats = 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 isExpired() { - return this.billing != null && this.billing.expiration != null && - new Date(this.billing.expiration) < new Date(); - } - - 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 invoices() { - return this.billing != null ? this.billing.invoices : null; - } - - get transactions() { - return this.billing != null ? this.billing.transactions : null; - } - - get paymentSource() { - return this.billing != null ? this.billing.paymentSource : 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() { - switch (this.billing.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; - } - } - - get canAdjustSeats() { - return this.billing.planType === PlanType.EnterpriseMonthly || - this.billing.planType === PlanType.EnterpriseAnnually || - this.billing.planType === PlanType.TeamsMonthly || this.billing.planType === PlanType.TeamsAnnually; - } - - get canDownloadLicense() { - return (this.billing.planType !== PlanType.Free && this.subscription == null) || - (this.subscription != null && !this.subscription.cancelled); - } } diff --git a/src/app/organizations/settings/organization-billing.component.html b/src/app/organizations/settings/organization-subscription.component.html similarity index 52% rename from src/app/organizations/settings/organization-billing.component.html rename to src/app/organizations/settings/organization-subscription.component.html index 25b1e44217..ee3f834a15 100644 --- a/src/app/organizations/settings/organization-billing.component.html +++ b/src/app/organizations/settings/organization-subscription.component.html @@ -1,13 +1,13 @@ - + {{'subscriptionCanceled' | i18n}}

{{'subscriptionPendingCanceled' | i18n}}

@@ -19,22 +19,22 @@
{{'billingPlan' | i18n}}
-
{{billing.plan}}
+
{{sub.plan}}
{{'expiration' | i18n}}
-
- {{billing.expiration | date:'mediumDate'}} +
+ {{sub.expiration | date:'mediumDate'}} {{'licenseIsExpired' | i18n}}
-
{{'neverExpires' | i18n}}
+
{{'neverExpires' | i18n}}
{{'billingPlan' | i18n}}
-
{{billing.plan}}
+
{{sub.plan}}
{{'status' | i18n}}
@@ -97,8 +97,8 @@

{{'userSeats' | i18n}}

-

{{'subscriptionUserSeats' | i18n : billing.seats}}

- +

{{'subscriptionUserSeats' | i18n : sub.seats}}

+

{{'storage' | i18n}}

-

{{'subscriptionStorage' | i18n : billing.maxStorageGb || 0 : billing.storageName || '0 MB'}}

+

{{'subscriptionStorage' | i18n : sub.maxStorageGb || 0 : sub.storageName || '0 MB'}}

{{(storagePercentage / 100) | percent}}
- +
-

{{'paymentMethod' | i18n}}

-

{{'noPaymentMethod' | i18n}}

- - -

{{'verifyBankAccountDesc' | i18n}} {{'verifyBankAccountFailureWarning' | i18n}}

-
- -
-
-
$0.
-
- -
- -
-
-
$0.
-
- -
- -
-
-

- - {{paymentSource.description}} -

-
- - - -

{{'invoices' | i18n}}

-

{{'noInvoices' | i18n}}

- - - - - - - - - -
{{i.date | date:'mediumDate'}} - - - - {{'invoiceNumber' | i18n : i.number}} - {{i.amount | currency:'$'}} - - - {{'paid' | i18n}} - - - - {{'unpaid' | i18n}} - -
-

{{'transactions' | i18n}}

-

{{'noTransactions' | i18n}}

- - - - - - - - - -
{{t.createdDate | date:'mediumDate'}} - {{'chargeNoun' | i18n}} - {{'chargeRefund' | i18n}} - - - {{t.details}} - {{t.amount | currency:'$'}}
- * {{'chargesStatement' | i18n : 'BITWARDEN'}} diff --git a/src/app/organizations/settings/organization-subscription.component.ts b/src/app/organizations/settings/organization-subscription.component.ts new file mode 100644 index 0000000000..f750ea10e4 --- /dev/null +++ b/src/app/organizations/settings/organization-subscription.component.ts @@ -0,0 +1,230 @@ +import { + Component, + OnInit, +} from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { OrganizationSubscriptionResponse } from 'jslib/models/response/organizationSubscriptionResponse'; + +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'; + +@Component({ + selector: 'app-org-subscription', + templateUrl: 'organization-subscription.component.html', +}) +export class OrganizationSubscriptionComponent implements OnInit { + loading = false; + firstLoaded = false; + organizationId: string; + adjustSeatsAdd = true; + showAdjustSeats = false; + adjustStorageAdd = true; + showAdjustStorage = false; + showUpdateLicense = false; + sub: OrganizationSubscriptionResponse; + selfHosted = false; + + cancelPromise: Promise; + reinstatePromise: Promise; + licensePromise: Promise; + + 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) { + 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.sub = await this.apiService.getOrganizationSubscription(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.postOrganizationReinstate(this.organizationId); + await this.reinstatePromise; + this.analytics.eventTrack.next({ action: 'Reinstated Plan' }); + 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.postOrganizationCancel(this.organizationId); + await this.cancelPromise; + this.analytics.eventTrack.next({ action: 'Canceled Plan' }); + this.toasterService.popAsync('success', null, this.i18nService.t('canceledSubscription')); + this.load(); + } catch { } + } + + async changePlan() { + const contactSupport = await this.platformUtilsService.showDialog(this.i18nService.t('changeBillingPlanDesc'), + this.i18nService.t('changeBillingPlan'), this.i18nService.t('contactSupport'), this.i18nService.t('close')); + if (contactSupport) { + this.platformUtilsService.launchUri('https://bitwarden.com/contact'); + } + } + + async downloadLicense() { + if (this.loading) { + return; + } + + const installationId = window.prompt(this.i18nService.t('enterInstallationId')); + if (installationId == null || installationId === '') { + return; + } + + try { + this.licensePromise = this.apiService.getOrganizationLicense(this.organizationId, installationId); + const license = await this.licensePromise; + const licenseString = JSON.stringify(license, null, 2); + this.platformUtilsService.saveFile(window, licenseString, null, 'bitwarden_organization_license.json'); + } catch { } + } + + updateLicense() { + if (this.loading) { + return; + } + this.showUpdateLicense = true; + } + + closeUpdateLicense(updated: boolean) { + this.showUpdateLicense = false; + if (updated) { + this.load(); + this.messagingService.send('updatedOrgLicense'); + } + } + + adjustSeats(add: boolean) { + this.adjustSeatsAdd = add; + this.showAdjustSeats = true; + } + + closeSeats(load: boolean) { + this.showAdjustSeats = false; + if (load) { + this.load(); + } + } + + adjustStorage(add: boolean) { + this.adjustStorageAdd = add; + this.showAdjustStorage = true; + } + + closeStorage(load: boolean) { + this.showAdjustStorage = false; + if (load) { + this.load(); + } + } + + get isExpired() { + return this.sub != null && this.sub.expiration != null && + new Date(this.sub.expiration) < new Date(); + } + + get subscriptionMarkedForCancel() { + return this.subscription != null && !this.subscription.cancelled && this.subscription.cancelAtEndDate; + } + + get subscription() { + return this.sub != null ? this.sub.subscription : null; + } + + get nextInvoice() { + return this.sub != null ? this.sub.upcomingInvoice : null; + } + + get storagePercentage() { + return this.sub != null && this.sub.maxStorageGb ? + +(100 * (this.sub.storageGb / this.sub.maxStorageGb)).toFixed(2) : 0; + } + + get storageProgressWidth() { + return this.storagePercentage < 5 ? 5 : 0; + } + + get billingInterval() { + const monthly = this.sub.planType === PlanType.EnterpriseMonthly || + this.sub.planType === PlanType.TeamsMonthly; + return monthly ? 'month' : 'year'; + } + + get storageGbPrice() { + return this.billingInterval === 'month' ? 0.5 : 4; + } + + 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; + } + } + + get canAdjustSeats() { + return this.sub.planType === PlanType.EnterpriseMonthly || + this.sub.planType === PlanType.EnterpriseAnnually || + this.sub.planType === PlanType.TeamsMonthly || this.sub.planType === PlanType.TeamsAnnually; + } + + get canDownloadLicense() { + return (this.sub.planType !== PlanType.Free && this.subscription == null) || + (this.subscription != null && !this.subscription.cancelled); + } +} diff --git a/src/app/organizations/settings/settings.component.html b/src/app/organizations/settings/settings.component.html index 68836ceab1..af65c40125 100644 --- a/src/app/organizations/settings/settings.component.html +++ b/src/app/organizations/settings/settings.component.html @@ -7,8 +7,11 @@ {{'myOrganization' | i18n}} + + {{'subscription' | i18n}} + - {{'billingAndLicensing' | i18n}} + {{'billing' | i18n}} {{'twoStepLogin' | i18n}} diff --git a/src/app/settings/premium.component.ts b/src/app/settings/premium.component.ts index ab9394bd6b..55596d317c 100644 --- a/src/app/settings/premium.component.ts +++ b/src/app/settings/premium.component.ts @@ -45,7 +45,7 @@ export class PremiumComponent implements OnInit { this.canAccessPremium = await this.userService.canAccessPremium(); const premium = await this.tokenService.getPremium(); if (premium) { - this.router.navigate(['/settings/billing']); + this.router.navigate(['/settings/subscription']); return; } } @@ -95,7 +95,7 @@ export class PremiumComponent implements OnInit { this.analytics.eventTrack.next({ action: 'Signed Up Premium' }); this.toasterService.popAsync('success', null, this.i18nService.t('premiumUpdated')); this.messagingService.send('purchasedPremium'); - this.router.navigate(['/settings/billing']); + this.router.navigate(['/settings/subscription']); } get additionalStorageTotal(): number { diff --git a/src/app/settings/settings.component.html b/src/app/settings/settings.component.html index 3b4e9dd3b6..7acfc03e2d 100644 --- a/src/app/settings/settings.component.html +++ b/src/app/settings/settings.component.html @@ -13,12 +13,15 @@ {{'organizations' | i18n}} - - {{'billingAndLicensing' | i18n}} + + {{'premiumMembership' | i18n}} {{'goPremium' | i18n}} + + {{'billing' | i18n}} + {{'twoStepLogin' | i18n}} diff --git a/src/app/settings/user-billing.component.html b/src/app/settings/user-billing.component.html index e8d9e2ffcf..63d1d3f4ce 100644 --- a/src/app/settings/user-billing.component.html +++ b/src/app/settings/user-billing.component.html @@ -1,6 +1,6 @@ - {{'subscriptionCanceled' | i18n}} - -

{{'subscriptionPendingCanceled' | i18n}}

- -
-
-
{{'subscription' | i18n}}
-
{{'premiumMembership' | i18n}}
-
-
-
{{'expiration' | i18n}}
-
{{billing.expiration | date:'mediumDate'}}
-
{{'neverExpires' | i18n}}
-
-
-
-
-
{{'status' | i18n}}
-
- {{(subscription && 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}} -
-
-
- -
- - - {{'manageSubscription' | i18n}} - -
-
-
-

{{'updateLicense' | i18n}}

- -
-
-
- -
- - -
-

{{'storage' | i18n}}

-

{{'subscriptionStorage' | i18n : billing.maxStorageGb || 0 : billing.storageName || '0 MB'}}

-
-
{{(storagePercentage / 100) | percent}}
-
- -
-
- - +

{{'paymentMethod' | i18n}}

+

{{'noPaymentMethod' | i18n}}

+ + +

{{'verifyBankAccountDesc' | i18n}} {{'verifyBankAccountFailureWarning' | i18n}}

+
+ +
+
+
$0.
+
+
- -
- -

{{'paymentMethod' | i18n}}

-

{{'noPaymentMethod' | i18n}}

-

+ +

+
+
$0.
+
+ +
+ + + +

+ 'fa-university': paymentSource.type === paymentMethodType.BankAccount, + 'fa-paypal text-primary': paymentSource.type === paymentMethodType.PayPal}"> {{paymentSource.description}}

- - - -

{{'invoices' | i18n}}

-

{{'noInvoices' | i18n}}

- - - - - - - - - -
{{i.date | date:'mediumDate'}} - - - - {{'invoiceNumber' | i18n : i.number}} - {{i.amount | currency:'$'}} - - - {{'paid' | i18n}} - - - - {{'unpaid' | i18n}} - -
-

{{'transactions' | i18n}}

-

{{'noTransactions' | i18n}}

- - - - - - + + + +
{{t.createdDate | date:'mediumDate'}} - {{'chargeNoun' | i18n}} - {{'chargeRefund' | i18n}} - - + {{(paymentSource ? 'changePaymentMethod' : 'addPaymentMethod') | i18n}} + + + +

{{'invoices' | i18n}}

+

{{'noInvoices' | i18n}}

+ + + + + + + + + +
{{i.date | date:'mediumDate'}} + + + + {{'invoiceNumber' | i18n : i.number}} + {{i.amount | currency:'$'}} + + + {{'paid' | i18n}} + + + + {{'unpaid' | i18n}} + +
+

{{'transactions' | i18n}}

+

{{'noTransactions' | i18n}}

+ + + + + + - - - -
{{t.createdDate | date:'mediumDate'}} + {{'chargeNoun' | i18n}} + {{'chargeRefund' | i18n}} + + - {{t.details}} - {{t.amount | currency:'$'}}
- * {{'chargesStatement' | i18n : 'BITWARDEN'}} - + {{t.details}} +
{{t.amount + | currency:'$'}}
+ * {{'chargesStatement' | i18n : 'BITWARDEN'}} diff --git a/src/app/settings/user-billing.component.ts b/src/app/settings/user-billing.component.ts index 210971246f..17e0b08032 100644 --- a/src/app/settings/user-billing.component.ts +++ b/src/app/settings/user-billing.component.ts @@ -2,7 +2,6 @@ import { Component, OnInit, } from '@angular/core'; -import { Router } from '@angular/router'; import { ToasterService } from 'angular2-toaster'; import { Angulartics2 } from 'angulartics2'; @@ -11,11 +10,10 @@ import { BillingResponse } from 'jslib/models/response/billingResponse'; 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 { TransactionType } from 'jslib/enums/transactionType'; +import { VerifyBankRequest } from 'jslib/models/request/verifyBankRequest'; @Component({ selector: 'app-user-billing', @@ -24,24 +22,18 @@ import { TransactionType } from 'jslib/enums/transactionType'; export class UserBillingComponent implements OnInit { loading = false; firstLoaded = false; - adjustStorageAdd = true; - showAdjustStorage = false; showAdjustPayment = false; - showUpdateLicense = false; billing: BillingResponse; paymentMethodType = PaymentMethodType; transactionType = TransactionType; - selfHosted = false; + organizationId: string; + verifyAmount1: number; + verifyAmount2: number; - cancelPromise: Promise; - reinstatePromise: Promise; + verifyBankPromise: Promise; - constructor(private tokenService: TokenService, private apiService: ApiService, - private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, - private analytics: Angulartics2, private toasterService: ToasterService, - private router: Router) { - this.selfHosted = platformUtilsService.isSelfHost(); - } + constructor(protected apiService: ApiService, protected i18nService: I18nService, + protected analytics: Angulartics2, protected toasterService: ToasterService) { } async ngOnInit() { await this.load(); @@ -52,93 +44,32 @@ export class UserBillingComponent implements OnInit { if (this.loading) { return; } - - if (this.tokenService.getPremium()) { - this.loading = true; - this.billing = await this.apiService.getUserBilling(); + this.loading = true; + if (this.organizationId != null) { + this.billing = await this.apiService.getOrganizationBilling(this.organizationId); } else { - this.router.navigate(['/settings/premium']); - return; + this.billing = await this.apiService.getUserBilling(); } - this.loading = false; } - async reinstate() { + async verifyBank() { 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')); + const request = new VerifyBankRequest(); + request.amount1 = this.verifyAmount1; + request.amount2 = this.verifyAmount2; + this.verifyBankPromise = this.apiService.postOrganizationVerifyBank(this.organizationId, request); + await this.verifyBankPromise; + this.analytics.eventTrack.next({ action: 'Verified Bank Account' }); + this.toasterService.popAsync('success', null, this.i18nService.t('verifiedBankAccount')); 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; - } - - const licenseString = JSON.stringify(this.billing.license, null, 2); - this.platformUtilsService.saveFile(window, licenseString, null, 'bitwarden_premium_license.json'); - } - - 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; } @@ -150,18 +81,6 @@ export class UserBillingComponent implements OnInit { } } - 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; } @@ -173,13 +92,4 @@ export class UserBillingComponent implements OnInit { get transactions() { return this.billing != null ? this.billing.transactions : 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; - } } diff --git a/src/app/settings/user-subscription.component.html b/src/app/settings/user-subscription.component.html new file mode 100644 index 0000000000..54bcbf270f --- /dev/null +++ b/src/app/settings/user-subscription.component.html @@ -0,0 +1,103 @@ + + + + {{'subscriptionCanceled' | i18n}} + +

{{'subscriptionPendingCanceled' | i18n}}

+ +
+
+
{{'expiration' | i18n}}
+
{{sub.expiration | date:'mediumDate'}}
+
{{'neverExpires' | i18n}}
+
+
+
+
+
{{'status' | i18n}}
+
+ {{(subscription && 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}} +
+
+
+ +
+ + + {{'manageSubscription' | i18n}} + +
+
+
+

{{'updateLicense' | i18n}}

+ +
+
+
+ +
+ + +
+

{{'storage' | i18n}}

+

{{'subscriptionStorage' | i18n : sub.maxStorageGb || 0 : sub.storageName || '0 MB'}}

+
+
{{(storagePercentage / 100) | percent}}
+
+ +
+
+ + +
+ +
+
+
+
diff --git a/src/app/settings/user-subscription.component.ts b/src/app/settings/user-subscription.component.ts new file mode 100644 index 0000000000..951a5f9545 --- /dev/null +++ b/src/app/settings/user-subscription.component.ts @@ -0,0 +1,156 @@ +import { + Component, + OnInit, +} from '@angular/core'; +import { Router } from '@angular/router'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { SubscriptionResponse } from 'jslib/models/response/subscriptionResponse'; + +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'; + +@Component({ + selector: 'app-user-subscription', + templateUrl: 'user-subscription.component.html', +}) +export class UserSubscriptionComponent implements OnInit { + loading = false; + firstLoaded = false; + adjustStorageAdd = true; + showAdjustStorage = false; + showUpdateLicense = false; + sub: SubscriptionResponse; + 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) { + this.selfHosted = platformUtilsService.isSelfHost(); + } + + async ngOnInit() { + await this.load(); + this.firstLoaded = true; + } + + async load() { + if (this.loading) { + return; + } + + if (this.tokenService.getPremium()) { + this.loading = true; + this.sub = await this.apiService.getUserSubscription(); + } else { + this.router.navigate(['/settings/premium']); + return; + } + + 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; + } + + const licenseString = JSON.stringify(this.sub.license, null, 2); + this.platformUtilsService.saveFile(window, licenseString, null, 'bitwarden_premium_license.json'); + } + + 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(); + } + } + + get subscriptionMarkedForCancel() { + return this.subscription != null && !this.subscription.cancelled && this.subscription.cancelAtEndDate; + } + + get subscription() { + return this.sub != null ? this.sub.subscription : null; + } + + get nextInvoice() { + return this.sub != null ? this.sub.upcomingInvoice : null; + } + + get storagePercentage() { + return this.sub != null && this.sub.maxStorageGb ? + +(100 * (this.sub.storageGb / this.sub.maxStorageGb)).toFixed(2) : 0; + } + + get storageProgressWidth() { + return this.storagePercentage < 5 ? 5 : 0; + } +} diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 64917d31fc..031c582673 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -1487,8 +1487,8 @@ "reportError": { "message": "An error occurred trying to load the report. Try again" }, - "billingAndLicensing": { - "message": "Billing & Licensing" + "billing": { + "message": "Billing" }, "goPremium": { "message": "Go Premium",