mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-20 02:01:47 +01:00
create and update premium license for self host
This commit is contained in:
parent
463c1f8b77
commit
3bb667f524
2
jslib
2
jslib
@ -1 +1 @@
|
|||||||
Subproject commit 20622db73c5c2a56777944bb06f32a21bf2e763f
|
Subproject commit 8be95bfe574a7ae2c8173921bbdfe82451436081
|
@ -54,6 +54,7 @@ import { TwoFactorSetupComponent } from './settings/two-factor-setup.component';
|
|||||||
import { TwoFactorU2fComponent } from './settings/two-factor-u2f.component';
|
import { TwoFactorU2fComponent } from './settings/two-factor-u2f.component';
|
||||||
import { TwoFactorVerifyComponent } from './settings/two-factor-verify.component';
|
import { TwoFactorVerifyComponent } from './settings/two-factor-verify.component';
|
||||||
import { TwoFactorYubiKeyComponent } from './settings/two-factor-yubikey.component';
|
import { TwoFactorYubiKeyComponent } from './settings/two-factor-yubikey.component';
|
||||||
|
import { UpdateLicenseComponent } from './settings/update-license.component';
|
||||||
import { UserBillingComponent } from './settings/user-billing.component';
|
import { UserBillingComponent } from './settings/user-billing.component';
|
||||||
|
|
||||||
import { BreachReportComponent } from './tools/breach-report.component';
|
import { BreachReportComponent } from './tools/breach-report.component';
|
||||||
@ -171,6 +172,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
|
|||||||
TwoFactorU2fComponent,
|
TwoFactorU2fComponent,
|
||||||
TwoFactorVerifyComponent,
|
TwoFactorVerifyComponent,
|
||||||
TwoFactorYubiKeyComponent,
|
TwoFactorYubiKeyComponent,
|
||||||
|
UpdateLicenseComponent,
|
||||||
UserBillingComponent,
|
UserBillingComponent,
|
||||||
UserLayoutComponent,
|
UserLayoutComponent,
|
||||||
VaultComponent,
|
VaultComponent,
|
||||||
|
@ -114,8 +114,8 @@ export function initFactory(): Function {
|
|||||||
const isDev = platformUtilsService.isDev();
|
const isDev = platformUtilsService.isDev();
|
||||||
await apiService.setUrls({
|
await apiService.setUrls({
|
||||||
base: isDev ? null : window.location.origin,
|
base: isDev ? null : window.location.origin,
|
||||||
api: isDev ? 'https://api.bitwarden.com' : null,
|
api: isDev ? 'http://localhost:4000' : null,
|
||||||
identity: isDev ? 'https://identity.bitwarden.com' : null,
|
identity: isDev ? 'http://localhost:33656' : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
lockService.init(true);
|
lockService.init(true);
|
||||||
|
@ -22,9 +22,17 @@
|
|||||||
{{'premiumSignUpFuture' | i18n}}
|
{{'premiumSignUpFuture' | i18n}}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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>
|
</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>
|
<h2 class="mt-5">{{'addons' | i18n}}</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group col-6">
|
<div class="form-group col-6">
|
||||||
|
@ -10,6 +10,7 @@ import { Angulartics2 } from 'angulartics2';
|
|||||||
|
|
||||||
import { ApiService } from 'jslib/abstractions/api.service';
|
import { ApiService } from 'jslib/abstractions/api.service';
|
||||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||||
|
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||||
|
|
||||||
import { PaymentComponent } from './payment.component';
|
import { PaymentComponent } from './payment.component';
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ export class PremiumComponent {
|
|||||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||||
@Output() onPremiumPurchased = new EventEmitter();
|
@Output() onPremiumPurchased = new EventEmitter();
|
||||||
|
|
||||||
|
selfHosted = false;
|
||||||
premiumPrice = 10;
|
premiumPrice = 10;
|
||||||
storageGbPrice = 4;
|
storageGbPrice = 4;
|
||||||
additionalStorage = 0;
|
additionalStorage = 0;
|
||||||
@ -28,7 +30,10 @@ export class PremiumComponent {
|
|||||||
formPromise: Promise<any>;
|
formPromise: Promise<any>;
|
||||||
|
|
||||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
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() {
|
async submit() {
|
||||||
try {
|
try {
|
||||||
@ -44,6 +49,13 @@ export class PremiumComponent {
|
|||||||
} catch { }
|
} 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 {
|
get additionalStorageTotal(): number {
|
||||||
return this.storageGbPrice * this.additionalStorage;
|
return this.storageGbPrice * this.additionalStorage;
|
||||||
}
|
}
|
||||||
@ -51,11 +63,4 @@ export class PremiumComponent {
|
|||||||
get total(): number {
|
get total(): number {
|
||||||
return this.additionalStorageTotal + this.premiumPrice;
|
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
14
src/app/settings/update-license.component.html
Normal file
14
src/app/settings/update-license.component.html
Normal 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>
|
68
src/app/settings/update-license.component.ts
Normal file
68
src/app/settings/update-license.component.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -9,7 +9,7 @@
|
|||||||
<app-premium *ngIf="!premium" (onPremiumPurchased)="load()"></app-premium>
|
<app-premium *ngIf="!premium" (onPremiumPurchased)="load()"></app-premium>
|
||||||
<i class="fa fa-spinner fa-spin text-muted" *ngIf="premium && !firstLoaded && loading"></i>
|
<i class="fa fa-spinner fa-spin text-muted" *ngIf="premium && !firstLoaded && loading"></i>
|
||||||
<ng-container *ngIf="premium && billing">
|
<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">
|
<app-callout type="warning" title="{{'pendingCancellation' | i18n}}" *ngIf="subscriptionMarkedForCancel">
|
||||||
<p>{{'subscriptionPendingCanceled' | i18n}}</p>
|
<p>{{'subscriptionPendingCanceled' | i18n}}</p>
|
||||||
<button #reinstateBtn type="button" class="btn btn-outline-secondary btn-submit" (click)="reinstate()" [appApiAction]="reinstatePromise"
|
<button #reinstateBtn type="button" class="btn btn-outline-secondary btn-submit" (click)="reinstate()" [appApiAction]="reinstatePromise"
|
||||||
@ -33,7 +33,8 @@
|
|||||||
</dd>
|
</dd>
|
||||||
<dt>{{'nextCharge' | i18n}}</dt>
|
<dt>{{'nextCharge' | i18n}}</dt>
|
||||||
<dd>{{nextInvoice ? ((nextInvoice.date | date: 'mediumDate') + ', ' + (nextInvoice.amount | currency:'$')) :
|
<dd>{{nextInvoice ? ((nextInvoice.date | date: 'mediumDate') + ', ' + (nextInvoice.amount | currency:'$')) :
|
||||||
'-'}}</dd>
|
'-'}}
|
||||||
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
@ -53,12 +54,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="selfHosted">
|
<ng-container *ngIf="selfHosted">
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="updateLicense()">
|
<div>
|
||||||
{{'updateLicense' | i18n}}
|
<button type="button" class="btn btn-outline-secondary" (click)="updateLicense()">
|
||||||
</button>
|
{{'updateLicense' | i18n}}
|
||||||
<a href="https://vault.bitwarden.com/#/settings/billing" target="_blank" rel="noopener" class="btn btn-outline-secondary">
|
</button>
|
||||||
{{'manageSubscription' | i18n}}
|
<a href="https://vault.bitwarden.com/#/settings/billing" target="_blank" rel="noopener" class="btn btn-outline-secondary">
|
||||||
</a>
|
{{'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>
|
||||||
<ng-container *ngIf="!selfHosted">
|
<ng-container *ngIf="!selfHosted">
|
||||||
<ng-container *ngIf="!subscription.cancelled || subscriptionMarkedForCancel">
|
<ng-container *ngIf="!subscription.cancelled || subscriptionMarkedForCancel">
|
||||||
|
@ -26,6 +26,7 @@ export class UserBillingComponent implements OnInit {
|
|||||||
adjustStorageAdd = true;
|
adjustStorageAdd = true;
|
||||||
showAdjustStorage = false;
|
showAdjustStorage = false;
|
||||||
showAdjustPayment = false;
|
showAdjustPayment = false;
|
||||||
|
showUpdateLicense = false;
|
||||||
billing: BillingResponse;
|
billing: BillingResponse;
|
||||||
paymentMethodType = PaymentMethodType;
|
paymentMethodType = PaymentMethodType;
|
||||||
selfHosted = false;
|
selfHosted = false;
|
||||||
@ -110,6 +111,14 @@ export class UserBillingComponent implements OnInit {
|
|||||||
if (this.loading) {
|
if (this.loading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.showUpdateLicense = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeUpdateLicense(load: boolean) {
|
||||||
|
this.showUpdateLicense = false;
|
||||||
|
if (load) {
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
adjustStorage(add: boolean) {
|
adjustStorage(add: boolean) {
|
||||||
|
@ -1331,6 +1331,9 @@
|
|||||||
"updateLicense": {
|
"updateLicense": {
|
||||||
"message": "Update License"
|
"message": "Update License"
|
||||||
},
|
},
|
||||||
|
"updatedLicense": {
|
||||||
|
"message": "Updated license"
|
||||||
|
},
|
||||||
"manageSubscription": {
|
"manageSubscription": {
|
||||||
"message": "Manage Subscription"
|
"message": "Manage Subscription"
|
||||||
},
|
},
|
||||||
@ -1413,5 +1416,26 @@
|
|||||||
},
|
},
|
||||||
"updatedPaymentMethod": {
|
"updatedPaymentMethod": {
|
||||||
"message": "Updated payment method."
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user