create and update premium license for self host

This commit is contained in:
Kyle Spearrin 2018-07-02 10:30:51 -04:00
parent 463c1f8b77
commit 3bb667f524
10 changed files with 160 additions and 21 deletions

2
jslib

@ -1 +1 @@
Subproject commit 20622db73c5c2a56777944bb06f32a21bf2e763f
Subproject commit 8be95bfe574a7ae2c8173921bbdfe82451436081

View File

@ -54,6 +54,7 @@ import { TwoFactorSetupComponent } from './settings/two-factor-setup.component';
import { TwoFactorU2fComponent } from './settings/two-factor-u2f.component';
import { TwoFactorVerifyComponent } from './settings/two-factor-verify.component';
import { TwoFactorYubiKeyComponent } from './settings/two-factor-yubikey.component';
import { UpdateLicenseComponent } from './settings/update-license.component';
import { UserBillingComponent } from './settings/user-billing.component';
import { BreachReportComponent } from './tools/breach-report.component';
@ -171,6 +172,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
TwoFactorU2fComponent,
TwoFactorVerifyComponent,
TwoFactorYubiKeyComponent,
UpdateLicenseComponent,
UserBillingComponent,
UserLayoutComponent,
VaultComponent,

View File

@ -114,8 +114,8 @@ export function initFactory(): Function {
const isDev = platformUtilsService.isDev();
await apiService.setUrls({
base: isDev ? null : window.location.origin,
api: isDev ? 'https://api.bitwarden.com' : null,
identity: isDev ? 'https://identity.bitwarden.com' : null,
api: isDev ? 'http://localhost:4000' : null,
identity: isDev ? 'http://localhost:33656' : null,
});
lockService.init(true);

View File

@ -22,9 +22,17 @@
{{'premiumSignUpFuture' | i18n}}
</li>
</ul>
<p class="text-lg mb-0">{{'premiumPrice' | i18n : (premiumPrice | currency:'$')}}</p>
<p class="text-lg" [ngClass]="{'mb-0':!selfHosted}">{{'premiumPrice' | i18n : (premiumPrice | currency:'$')}}</p>
<a href="https://vault.bitwarden.com/#/settings/billing" target="_blank" rel="noopener" class="btn btn-outline-secondary"
*ngIf="selfHosted">
{{'purchasePremium' | i18n}}
</a>
</app-callout>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<ng-container *ngIf="selfHosted">
<p>{{'uploadLicenseFilePremium' | i18n}}</p>
<app-update-license [user]="true" [create]="true" (onUpdated)="finalizePremium()"></app-update-license>
</ng-container>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="!selfHosted">
<h2 class="mt-5">{{'addons' | i18n}}</h2>
<div class="row">
<div class="form-group col-6">

View File

@ -10,6 +10,7 @@ import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { PaymentComponent } from './payment.component';
@ -21,6 +22,7 @@ export class PremiumComponent {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@Output() onPremiumPurchased = new EventEmitter();
selfHosted = false;
premiumPrice = 10;
storageGbPrice = 4;
additionalStorage = 0;
@ -28,7 +30,10 @@ export class PremiumComponent {
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService) { }
private analytics: Angulartics2, private toasterService: ToasterService,
platformUtilsService: PlatformUtilsService) {
this.selfHosted = platformUtilsService.isSelfHost();
}
async submit() {
try {
@ -44,6 +49,13 @@ export class PremiumComponent {
} catch { }
}
async finalizePremium() {
await this.apiService.refreshIdentityToken();
this.analytics.eventTrack.next({ action: 'Signed Up Premium' });
this.toasterService.popAsync('success', null, this.i18nService.t('premiumUpdated'));
this.onPremiumPurchased.emit();
}
get additionalStorageTotal(): number {
return this.storageGbPrice * this.additionalStorage;
}
@ -51,11 +63,4 @@ export class PremiumComponent {
get total(): number {
return this.additionalStorageTotal + this.premiumPrice;
}
private async finalizePremium() {
await this.apiService.refreshIdentityToken();
this.analytics.eventTrack.next({ action: 'Signed Up Premium' });
this.toasterService.popAsync('success', null, this.i18nService.t('premiumUpdated'));
this.onPremiumPurchased.emit();
}
}

View File

@ -0,0 +1,14 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="form-group">
<label for="file">{{'licenseFile' | i18n}}</label>
<input type="file" id="file" class="form-control-file" name="file" required>
<small class="form-text text-muted">{{'licenseFileDesc' | i18n : (user ? 'bitwarden_premium_license.json' : 'bitwarden_organization_license.json')}}</small>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin"></i>
<span>{{'submit' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" *ngIf="!create">
{{'cancel' | i18n}}
</button>
</form>

View File

@ -0,0 +1,68 @@
import {
Component,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { TokenService } from 'jslib/abstractions/token.service';
@Component({
selector: 'app-update-license',
templateUrl: 'update-license.component.html',
})
export class UpdateLicenseComponent {
@Input() create = true;
@Input() user = true;
@Output() onUpdated = new EventEmitter();
@Output() onCanceled = new EventEmitter();
storageAdjustment = 0;
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private tokenService: TokenService) { }
async submit() {
const fileEl = document.getElementById('file') as HTMLInputElement;
const files = fileEl.files;
if (files == null || files.length === 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('selectFile'));
return;
}
try {
if (this.user) {
const fd = new FormData();
fd.append('license', files[0]);
if (this.create) {
if (!this.tokenService.getEmailVerified()) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('accountEmailMustBeVerified'));
return;
}
this.formPromise = this.apiService.postPremium(fd);
} else {
this.formPromise = this.apiService.postAccountLicense(fd);
}
}
await this.formPromise;
if (!this.create) {
this.analytics.eventTrack.next({ action: 'Updated License' });
this.toasterService.popAsync('success', null, this.i18nService.t('updatedLicense'));
}
this.onUpdated.emit();
} catch { }
}
cancel() {
this.onCanceled.emit();
}
}

View File

@ -9,7 +9,7 @@
<app-premium *ngIf="!premium" (onPremiumPurchased)="load()"></app-premium>
<i class="fa fa-spinner fa-spin text-muted" *ngIf="premium && !firstLoaded && loading"></i>
<ng-container *ngIf="premium && billing">
<app-callout type="warning" title="{{'canceled' | i18n}}" *ngIf="subscription.cancelled">{{'subscriptionCanceled' | i18n}}</app-callout>
<app-callout type="warning" title="{{'canceled' | i18n}}" *ngIf="subscription && subscription.cancelled">{{'subscriptionCanceled' | i18n}}</app-callout>
<app-callout type="warning" title="{{'pendingCancellation' | i18n}}" *ngIf="subscriptionMarkedForCancel">
<p>{{'subscriptionPendingCanceled' | i18n}}</p>
<button #reinstateBtn type="button" class="btn btn-outline-secondary btn-submit" (click)="reinstate()" [appApiAction]="reinstatePromise"
@ -33,7 +33,8 @@
</dd>
<dt>{{'nextCharge' | i18n}}</dt>
<dd>{{nextInvoice ? ((nextInvoice.date | date: 'mediumDate') + ', ' + (nextInvoice.amount | currency:'$')) :
'-'}}</dd>
'-'}}
</dd>
</dl>
</div>
<div class="col-8">
@ -53,12 +54,20 @@
</div>
</div>
<ng-container *ngIf="selfHosted">
<button type="button" class="btn btn-outline-secondary" (click)="updateLicense()">
{{'updateLicense' | i18n}}
</button>
<a href="https://vault.bitwarden.com/#/settings/billing" target="_blank" rel="noopener" class="btn btn-outline-secondary">
{{'manageSubscription' | i18n}}
</a>
<div>
<button type="button" class="btn btn-outline-secondary" (click)="updateLicense()">
{{'updateLicense' | i18n}}
</button>
<a href="https://vault.bitwarden.com/#/settings/billing" target="_blank" rel="noopener" class="btn btn-outline-secondary">
{{'manageSubscription' | i18n}}
</a>
</div>
<div class="card mt-3" *ngIf="showUpdateLicense">
<div class="card-body">
<h3 class="card-body-header">{{'updateLicense' | i18n}}</h3>
<app-update-license [create]="false" [user]="true" (onUpdated)="closeUpdateLicense(true)" (onCanceled)="closeUpdateLicense(false)"></app-update-license>
</div>
</div>
</ng-container>
<ng-container *ngIf="!selfHosted">
<ng-container *ngIf="!subscription.cancelled || subscriptionMarkedForCancel">

View File

@ -26,6 +26,7 @@ export class UserBillingComponent implements OnInit {
adjustStorageAdd = true;
showAdjustStorage = false;
showAdjustPayment = false;
showUpdateLicense = false;
billing: BillingResponse;
paymentMethodType = PaymentMethodType;
selfHosted = false;
@ -110,6 +111,14 @@ export class UserBillingComponent implements OnInit {
if (this.loading) {
return;
}
this.showUpdateLicense = true;
}
closeUpdateLicense(load: boolean) {
this.showUpdateLicense = false;
if (load) {
this.load();
}
}
adjustStorage(add: boolean) {

View File

@ -1331,6 +1331,9 @@
"updateLicense": {
"message": "Update License"
},
"updatedLicense": {
"message": "Updated license"
},
"manageSubscription": {
"message": "Manage Subscription"
},
@ -1413,5 +1416,26 @@
},
"updatedPaymentMethod": {
"message": "Updated payment method."
},
"purchasePremium": {
"message": "Purchase Premium"
},
"licenseFile": {
"message": "License File"
},
"licenseFileDesc": {
"message": "Your license file will be named something like $FILE_NAME$",
"placeholders": {
"file_name": {
"content": "$1",
"example": "bitwarden_premium_license.json"
}
}
},
"uploadLicenseFilePremium": {
"message": "To upgrade your account to a premium membership you need to upload a valid license file."
},
"accountEmailMustBeVerified": {
"message": "Your account's email address must be verified."
}
}