From f7b9416460afe80cb01882ee1dc32bbd125c048e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 29 Jun 2018 16:55:54 -0400 Subject: [PATCH] user billing page --- jslib | 2 +- src/app/settings/premium.component.html | 12 +- src/app/settings/user-billing.component.html | 106 ++++++++++++++++- src/app/settings/user-billing.component.ts | 116 ++++++++++++++++++- src/app/vault/ciphers.component.html | 3 +- src/locales/en/messages.json | 101 ++++++++++++++++ src/scss/styles.scss | 10 ++ 7 files changed, 337 insertions(+), 13 deletions(-) diff --git a/jslib b/jslib index ac221d88..ef897695 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit ac221d8867c526eb14077398d95399f9016378fa +Subproject commit ef897695e9b5bfecbfcbbd4ad3aec62b4ecdca25 diff --git a/src/app/settings/premium.component.html b/src/app/settings/premium.component.html index 0e15bb53..d5756c01 100644 --- a/src/app/settings/premium.component.html +++ b/src/app/settings/premium.component.html @@ -22,7 +22,7 @@ {{'premiumSignUpFuture' | i18n}} -

{{'premiumPrice' | i18n : (premiumPrice | currency:'USD':'$')}}

+

{{'premiumPrice' | i18n : (premiumPrice | currency:'$')}}

{{'addons' | i18n}}

@@ -31,15 +31,15 @@ - {{'additionalStorageDesc' | i18n : (storageGbPrice | currency:'USD')}} + {{'additionalStorageDesc' | i18n : (storageGbPrice | currency:'$')}}

{{'summary' | i18n}}

- {{'premiumMembership' | i18n}}: {{premiumPrice | currency:'USD':'$'}} -
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} GB × {{storageGbPrice | currency:'USD'}} = {{additionalStorageTotal - | currency:'USD':'$'}} + {{'premiumMembership' | i18n}}: {{premiumPrice | currency:'$'}} +
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} GB × {{storageGbPrice | currency:'$'}} = {{additionalStorageTotal + | currency:'$'}}
- {{'total' | i18n}}: USD {{total | currency:'USD'}} /{{'year' | i18n}} + {{'total' | i18n}}: {{total | currency:'USD $'}} /{{'year' | i18n}}
{{'paymentChargedAnnually' | i18n}}

{{'paymentInformation' | i18n}}

diff --git a/src/app/settings/user-billing.component.html b/src/app/settings/user-billing.component.html index 24388425..9b01a2d5 100644 --- a/src/app/settings/user-billing.component.html +++ b/src/app/settings/user-billing.component.html @@ -1,4 +1,106 @@ - + + + + + {{'subscriptionCanceled' | i18n}} + +

{{'subscriptionPendingCanceled' | 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}} +
+
+
+ +
+ + +
+
+ +

{{'storage' | i18n}}

+

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

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

{{'paymentMethod' | i18n}}

+

{{'noPaymentMethod' | i18n}}

+

+ + {{paymentSource.description}} +

+ +

{{'charges' | i18n}}

+

{{'noCharges' | i18n}}

+ + + + + + + + + +
{{c.createdDate | date:'mediumDate'}}{{c.paymentSource ? c.paymentSource.description : '-'}}{{c.status}}{{c.amount | currency:'$'}}
+ * {{'chargesStatement' | i18n : 'BITWARDEN'}} +
+
+
diff --git a/src/app/settings/user-billing.component.ts b/src/app/settings/user-billing.component.ts index b89b08c7..54b6773f 100644 --- a/src/app/settings/user-billing.component.ts +++ b/src/app/settings/user-billing.component.ts @@ -3,22 +3,132 @@ import { OnInit, } from '@angular/core'; +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +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'; + @Component({ selector: 'app-user-billing', templateUrl: 'user-billing.component.html', }) export class UserBillingComponent implements OnInit { premium = false; + loading = false; + firstLoaded = false; + billing: BillingResponse; + paymentMethodType = PaymentMethodType; - constructor(private tokenService: TokenService) { } + cancelPromise: Promise; + reinstatePromise: Promise; + + constructor(private tokenService: TokenService, private apiService: ApiService, + private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, + private analytics: Angulartics2, private toasterService: ToasterService) { } async ngOnInit() { - this.loadPremiumStatus(); + await this.load(); + this.firstLoaded = true; } - loadPremiumStatus() { + async load() { + if (this.loading) { + return; + } + this.premium = this.tokenService.getPremium(); + if (this.premium) { + this.loading = true; + this.billing = await this.apiService.getUserBilling(); + } + 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 { } + } + + license() { + if (this.loading) { + return; + } + + const licenseString = JSON.stringify(this.billing.license, null, 2); + this.platformUtilsService.saveFile(window, licenseString, null, 'bitwarden_premium_license.json'); + } + + adjustStorage(add: boolean) { + + } + + changePayment() { + + } + + 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 ? +(100 * (this.billing.storageGb / this.billing.maxStorageGb)).toFixed(2) : 0; } } diff --git a/src/app/vault/ciphers.component.html b/src/app/vault/ciphers.component.html index e9d1f9cf..fbfc66f1 100644 --- a/src/app/vault/ciphers.component.html +++ b/src/app/vault/ciphers.component.html @@ -52,7 +52,8 @@

{{'noItemsInList' | i18n}}

- +
diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 8c7521bc..344c73f7 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -215,6 +215,9 @@ "cancel": { "message": "Cancel" }, + "canceled": { + "message": "Canceled" + }, "close": { "message": "Close" }, @@ -1282,5 +1285,103 @@ }, "paypalClickSubmit": { "message": "Click the PayPal button to log into your PayPal account, then click the Submit button below to continue." + }, + "cancelSubscription": { + "message": "Cancel Subscription" + }, + "subscriptionCanceled": { + "message": "The subscription has been canceled." + }, + "pendingCancellation": { + "message": "Pending Cancellation" + }, + "subscriptionPendingCanceled": { + "message": "The subscription has been marked for cancellation at the end of the current billing period." + }, + "reinstateSubscription": { + "message": "Reinstate Subscription" + }, + "reinstateConfirmation": { + "message": "Are you sure you want to remove the pending cancellation request and reinstate your subscription?" + }, + "reinstated": { + "message": "The subscription has been reinstated." + }, + "cancelConfirmation": { + "message": "Are you sure you want to cancel? You will lose access to all of this subscription's features at the end of this billing cycle." + }, + "canceledSubscription": { + "message": "The subscription has been canceled." + }, + "neverExpires": { + "message": "Never Expires" + }, + "status": { + "message": "Status" + }, + "nextCharge": { + "message": "Next Charge" + }, + "details": { + "message": "Details" + }, + "downloadLicense": { + "message": "Download License" + }, + "updateLicense": { + "message": "Update License" + }, + "manageSubscription": { + "message": "Manage Subscription" + }, + "storage": { + "message": "Storage" + }, + "addStorage": { + "message": "Add Storage" + }, + "removeStorage": { + "message": "Remove Storage" + }, + "subscriptionStorage": { + "message": "Your subscription has a total of $MAX_STORAGE$ GB of encrypted file storage. You are currently using $USED_STORAGE$.", + "placeholders": { + "max_storage": { + "content": "$1", + "example": "4" + }, + "used_storage": { + "content": "$2", + "example": "65 MB" + } + } + }, + "paymentMethod": { + "message": "Payment Method" + }, + "noPaymentMethod": { + "message": "No payment method on file." + }, + "addPaymentMethod": { + "message": "Add Payment Method" + }, + "changePaymentMethod": { + "message": "Change Payment Method" + }, + "charges": { + "message": "Charges", + "description": "Credit card charges/payments." + }, + "noCharges": { + "message": "No charges." + }, + "chargesStatement": { + "message": "Any charges will appear on your statement as $STATEMENT_NAME$.", + "placeholders": { + "statement_name": { + "content": "$1", + "example": "BITWARDEN" + } + } } } diff --git a/src/scss/styles.scss b/src/scss/styles.scss index b183cf54..6aa34c9e 100644 --- a/src/scss/styles.scss +++ b/src/scss/styles.scss @@ -383,6 +383,16 @@ app-avatar { } } +app-user-billing { + .progress { + height: 20px; + + .progress-bar { + min-width: 50px; + } + } +} + #duo-frame { background: url('../images/loading.svg') 0 0 no-repeat; height: 330px;