premium and paid org callouts

This commit is contained in:
Kyle Spearrin 2018-07-18 09:21:23 -04:00
parent 1f6dd079cd
commit 1cee1c6e8f
10 changed files with 106 additions and 23 deletions

View File

@ -101,7 +101,24 @@ export class AppComponent implements OnDestroy, OnInit {
break; break;
case 'syncCompleted': case 'syncCompleted':
break; break;
case 'upgradeOrganization':
const upgradeConfirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('upgradeOrganizationDesc'), this.i18nService.t('upgradeOrganization'),
this.i18nService.t('upgradeOrganization'), this.i18nService.t('cancel'));
if (upgradeConfirmed) {
this.router.navigate(['organizations', message.organizationId, 'settings', 'billing']);
}
break;
case 'premiumRequired':
const premiumConfirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('premiumRequiredDesc'), this.i18nService.t('premiumRequired'),
this.i18nService.t('learnMore'), this.i18nService.t('cancel'));
if (premiumConfirmed) {
this.router.navigate(['settings/premium']);
}
break;
default: default:
break;
} }
}); });
}); });

View File

@ -11,6 +11,7 @@ import { AuditService } from 'jslib/abstractions/audit.service';
import { CipherService } from 'jslib/abstractions/cipher.service'; import { CipherService } from 'jslib/abstractions/cipher.service';
import { FolderService } from 'jslib/abstractions/folder.service'; import { FolderService } from 'jslib/abstractions/folder.service';
import { I18nService } from 'jslib/abstractions/i18n.service'; import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service'; import { StateService } from 'jslib/abstractions/state.service';
@ -36,9 +37,11 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
analytics: Angulartics2, toasterService: ToasterService, analytics: Angulartics2, toasterService: ToasterService,
auditService: AuditService, stateService: StateService, auditService: AuditService, stateService: StateService,
tokenService: TokenService, totpService: TotpService, tokenService: TokenService, totpService: TotpService,
passwordGenerationService: PasswordGenerationService, private apiService: ApiService) { passwordGenerationService: PasswordGenerationService, private apiService: ApiService,
messagingService: MessagingService) {
super(cipherService, folderService, i18nService, platformUtilsService, analytics, super(cipherService, folderService, i18nService, platformUtilsService, analytics,
toasterService, auditService, stateService, tokenService, totpService, passwordGenerationService); toasterService, auditService, stateService, tokenService, totpService, passwordGenerationService,
messagingService);
} }
protected async loadCipher() { protected async loadCipher() {

View File

@ -12,6 +12,7 @@ import {
} from '@angular/router'; } from '@angular/router';
import { I18nService } from 'jslib/abstractions/i18n.service'; import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { SyncService } from 'jslib/abstractions/sync.service'; import { SyncService } from 'jslib/abstractions/sync.service';
import { UserService } from 'jslib/abstractions/user.service'; import { UserService } from 'jslib/abstractions/user.service';
@ -50,7 +51,7 @@ export class VaultComponent implements OnInit {
constructor(private route: ActivatedRoute, private userService: UserService, constructor(private route: ActivatedRoute, private userService: UserService,
private location: Location, private router: Router, private location: Location, private router: Router,
private syncService: SyncService, private i18nService: I18nService, private syncService: SyncService, private i18nService: I18nService,
private componentFactoryResolver: ComponentFactoryResolver) { } private componentFactoryResolver: ComponentFactoryResolver, private messagingService: MessagingService) { }
ngOnInit() { ngOnInit() {
this.route.parent.params.subscribe(async (params) => { this.route.parent.params.subscribe(async (params) => {
@ -139,6 +140,11 @@ export class VaultComponent implements OnInit {
} }
editCipherAttachments(cipher: CipherView) { editCipherAttachments(cipher: CipherView) {
if (this.organization.maxStorageGb == null || this.organization.maxStorageGb === 0) {
this.messagingService.send('upgradeOrganization', { organizationId: cipher.organizationId });
return;
}
if (this.modal != null) { if (this.modal != null) {
this.modal.close(); this.modal.close();
} }

View File

@ -20,7 +20,7 @@
<h3 class="mb-0"> <h3 class="mb-0">
{{p.name}} {{p.name}}
<i class="fa fa-check text-success fa-fw" *ngIf="p.enabled" title="{{'enabled' | i18n}}"></i> <i class="fa fa-check text-success fa-fw" *ngIf="p.enabled" title="{{'enabled' | i18n}}"></i>
<a href="#" appStopClick class="badge badge-primary" *ngIf="!premium && p.premium"> <a href="#" appStopClick class="badge badge-primary" *ngIf="!premium && p.premium" (click)="premiumRequired()">
{{'premium' | i18n}} {{'premium' | i18n}}
</a> </a>
</h3> </h3>

View File

@ -8,6 +8,7 @@ import {
} from '@angular/core'; } from '@angular/core';
import { ApiService } from 'jslib/abstractions/api.service'; import { ApiService } from 'jslib/abstractions/api.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { TokenService } from 'jslib/abstractions/token.service'; import { TokenService } from 'jslib/abstractions/token.service';
import { TwoFactorProviders } from 'jslib/services/auth.service'; import { TwoFactorProviders } from 'jslib/services/auth.service';
@ -42,7 +43,7 @@ export class TwoFactorSetupComponent implements OnInit {
private modal: ModalComponent = null; private modal: ModalComponent = null;
constructor(private apiService: ApiService, private tokenService: TokenService, constructor(private apiService: ApiService, private tokenService: TokenService,
private componentFactoryResolver: ComponentFactoryResolver) { } private componentFactoryResolver: ComponentFactoryResolver, private messagingService: MessagingService) { }
async ngOnInit() { async ngOnInit() {
this.premium = this.tokenService.getPremium(); this.premium = this.tokenService.getPremium();
@ -125,6 +126,14 @@ export class TwoFactorSetupComponent implements OnInit {
this.openModal(this.recoveryModalRef, TwoFactorRecoveryComponent); this.openModal(this.recoveryModalRef, TwoFactorRecoveryComponent);
} }
async premiumRequired() {
const premium = await this.tokenService.getPremium();
if (!premium) {
this.messagingService.send('premiumRequired');
return;
}
}
private openModal<T>(ref: ViewContainerRef, type: Type<T>): T { private openModal<T>(ref: ViewContainerRef, type: Type<T>): T {
if (this.modal != null) { if (this.modal != null) {
this.modal.close(); this.modal.close();

View File

@ -50,8 +50,7 @@
<a href="#" class="d-block mr-2" appStopClick title="{{'generatePassword' | i18n}}" (click)="generatePassword()"> <a href="#" class="d-block mr-2" appStopClick title="{{'generatePassword' | i18n}}" (click)="generatePassword()">
<i class="fa fa-lg fa-fw fa-refresh"></i> <i class="fa fa-lg fa-fw fa-refresh"></i>
</a> </a>
<a href="#" class="d-block" #checkPasswordBtn appStopClick title="{{'checkPassword' | i18n}}" (click)="checkPassword()" <a href="#" class="d-block" #checkPasswordBtn appStopClick title="{{'checkPassword' | i18n}}" (click)="checkPassword()" [appApiAction]="checkPasswordPromise">
[appApiAction]="checkPasswordPromise">
<i class="fa fa-lg fa-fw fa-check-circle" [hidden]="checkPasswordBtn.loading"></i> <i class="fa fa-lg fa-fw fa-check-circle" [hidden]="checkPasswordBtn.loading"></i>
<i class="fa fa-lg fa-fw fa-spinner fa-spin" [hidden]="!checkPasswordBtn.loading" title="{{'loading' | i18n}}"></i> <i class="fa fa-lg fa-fw fa-spinner fa-spin" [hidden]="!checkPasswordBtn.loading" title="{{'loading' | i18n}}"></i>
</a> </a>
@ -81,6 +80,12 @@
<div class="col-6 form-group totp d-flex align-items-end" [ngClass]="{'low': totpLow}"> <div class="col-6 form-group totp d-flex align-items-end" [ngClass]="{'low': totpLow}">
<div *ngIf="!cipher.login.totp || !totpCode"> <div *ngIf="!cipher.login.totp || !totpCode">
<img src="../../images/totp-countdown.png" title="{{'verificationCodeTotp' | i18n}}" class="ml-2"> <img src="../../images/totp-countdown.png" title="{{'verificationCodeTotp' | i18n}}" class="ml-2">
<a href="#" appStopClick class="badge badge-primary ml-3" (click)="premiumRequired()" *ngIf="!cipher.organizationId && !isPremium">
{{'premium' | i18n}}
</a>
<a href="#" appStopClick class="badge badge-primary ml-3" (click)="upgradeOrganization()" *ngIf="cipher.organizationId && !organizationUseTotp">
{{'upgrade' | i18n}}
</a>
</div> </div>
<div *ngIf="cipher.login.totp && totpCode" class="d-flex align-items-center"> <div *ngIf="cipher.login.totp && totpCode" class="d-flex align-items-center">
<span class="totp-countdown mr-3 ml-2"> <span class="totp-countdown mr-3 ml-2">
@ -364,8 +369,8 @@
<button *ngIf="!organization" type="button" (click)="toggleFavorite()" class="btn btn-link" title="{{(cipher.favorite ? 'unfavorite' : 'favorite') | i18n}}"> <button *ngIf="!organization" type="button" (click)="toggleFavorite()" class="btn btn-link" title="{{(cipher.favorite ? 'unfavorite' : 'favorite') | i18n}}">
<i class="fa fa-lg" [ngClass]="{'fa-star': cipher.favorite, 'fa-star-o': !cipher.favorite}"></i> <i class="fa fa-lg" [ngClass]="{'fa-star': cipher.favorite, 'fa-star-o': !cipher.favorite}"></i>
</button> </button>
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger" title="{{'delete' | i18n}}" <button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger" title="{{'delete' | i18n}}" *ngIf="editMode"
*ngIf="editMode" [disabled]="deleteBtn.loading" [appApiAction]="deletePromise"> [disabled]="deleteBtn.loading" [appApiAction]="deletePromise">
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading"></i> <i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading" title="{{'loading' | i18n}}"></i> <i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading" title="{{'loading' | i18n}}"></i>
</button> </button>

View File

@ -12,6 +12,7 @@ import { AuditService } from 'jslib/abstractions/audit.service';
import { CipherService } from 'jslib/abstractions/cipher.service'; import { CipherService } from 'jslib/abstractions/cipher.service';
import { FolderService } from 'jslib/abstractions/folder.service'; import { FolderService } from 'jslib/abstractions/folder.service';
import { I18nService } from 'jslib/abstractions/i18n.service'; import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { StateService } from 'jslib/abstractions/state.service'; import { StateService } from 'jslib/abstractions/state.service';
@ -40,7 +41,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
analytics: Angulartics2, toasterService: ToasterService, analytics: Angulartics2, toasterService: ToasterService,
auditService: AuditService, stateService: StateService, auditService: AuditService, stateService: StateService,
protected tokenService: TokenService, protected totpService: TotpService, protected tokenService: TokenService, protected totpService: TotpService,
protected passwordGenerationService: PasswordGenerationService) { protected passwordGenerationService: PasswordGenerationService, protected messagingService: MessagingService) {
super(cipherService, folderService, i18nService, platformUtilsService, analytics, super(cipherService, folderService, i18nService, platformUtilsService, analytics,
toasterService, auditService, stateService); toasterService, auditService, stateService);
} }
@ -94,6 +95,18 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit {
return confirmed; return confirmed;
} }
async premiumRequired() {
const premium = await this.tokenService.getPremium();
if (!premium) {
this.messagingService.send('premiumRequired');
return;
}
}
async upgradeOrganization() {
this.messagingService.send('upgradeOrganization', { organizationId: this.cipher.organizationId });
}
protected cleanUp() { protected cleanUp() {
if (this.totpInterval) { if (this.totpInterval) {
window.clearInterval(this.totpInterval); window.clearInterval(this.totpInterval);

View File

@ -49,6 +49,17 @@
</app-vault-ciphers> </app-vault-ciphers>
</div> </div>
<div class="col-3"> <div class="col-3">
<div class="card border-warning mb-4" *ngIf="showUpdateKey">
<div class="card-header bg-warning text-white">
<i class="fa fa-warning fa-fw"></i> {{'updateKeyTitle' | i18n}}
</div>
<div class="card-body">
<p>{{'updateEncryptionKeyShortDesc' | i18n}}</p>
<button class="btn btn-block btn-outline-secondary" type="button" (click)="updateKey()">
{{'updateEncryptionKey' | i18n}}
</button>
</div>
</div>
<app-verify-email *ngIf="showVerifyEmail" class="d-block mb-4"></app-verify-email> <app-verify-email *ngIf="showVerifyEmail" class="d-block mb-4"></app-verify-email>
<div class="card border-warning mb-4" *ngIf="showBrowserOutdated"> <div class="card border-warning mb-4" *ngIf="showBrowserOutdated">
<div class="card-header bg-warning text-white"> <div class="card-header bg-warning text-white">
@ -61,17 +72,6 @@
</a> </a>
</div> </div>
</div> </div>
<div class="card border-warning mb-4" *ngIf="showUpdateKey">
<div class="card-header bg-warning text-white">
<i class="fa fa-warning fa-fw"></i> {{'updateKeyTitle' | i18n}}
</div>
<div class="card-body">
<p>{{'updateEncryptionKeyShortDesc' | i18n}}</p>
<button class="btn btn-block btn-outline-secondary" type="button" (click)="updateKey()">
{{'updateEncryptionKey' | i18n}}
</button>
</div>
</div>
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
{{'organizations' | i18n}} {{'organizations' | i18n}}

View File

@ -32,8 +32,10 @@ import { ShareComponent } from './share.component';
import { CryptoService } from 'jslib/abstractions/crypto.service'; import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service'; import { I18nService } from 'jslib/abstractions/i18n.service';
import { MessagingService } from 'jslib/abstractions/messaging.service';
import { SyncService } from 'jslib/abstractions/sync.service'; import { SyncService } from 'jslib/abstractions/sync.service';
import { TokenService } from 'jslib/abstractions/token.service'; import { TokenService } from 'jslib/abstractions/token.service';
import { UserService } from 'jslib/abstractions/user.service';
@Component({ @Component({
selector: 'app-vault', selector: 'app-vault',
@ -66,7 +68,8 @@ export class VaultComponent implements OnInit {
constructor(private syncService: SyncService, private route: ActivatedRoute, constructor(private syncService: SyncService, private route: ActivatedRoute,
private router: Router, private location: Location, private router: Router, private location: Location,
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver, private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
private tokenService: TokenService, private cryptoService: CryptoService) { } private tokenService: TokenService, private cryptoService: CryptoService,
private messagingService: MessagingService, private userService: UserService) { }
async ngOnInit() { async ngOnInit() {
this.showVerifyEmail = !(await this.tokenService.getEmailVerified()); this.showVerifyEmail = !(await this.tokenService.getEmailVerified());
@ -157,7 +160,19 @@ export class VaultComponent implements OnInit {
this.ciphersComponent.searchText = searchText; this.ciphersComponent.searchText = searchText;
} }
editCipherAttachments(cipher: CipherView) { async editCipherAttachments(cipher: CipherView) {
const premium = await this.tokenService.getPremium();
if (cipher.organizationId == null && !premium) {
this.messagingService.send('premiumRequired');
return;
} else if (cipher.organizationId != null) {
const org = await this.userService.getOrganization(cipher.organizationId);
if (org != null && (org.maxStorageGb == null || org.maxStorageGb === 0)) {
this.messagingService.send('upgradeOrganization', { organizationId: cipher.organizationId });
return;
}
}
if (this.modal != null) { if (this.modal != null) {
this.modal.close(); this.modal.close();
} }

View File

@ -1026,6 +1026,12 @@
"premiumMembership": { "premiumMembership": {
"message": "Premium Membership" "message": "Premium Membership"
}, },
"premiumRequired": {
"message": "Premium Required"
},
"premiumRequiredDesc": {
"message": "A premium membership is required to use this feature."
},
"manage": { "manage": {
"message": "Manage" "message": "Manage"
}, },
@ -2301,5 +2307,14 @@
}, },
"loading": { "loading": {
"message": "Loading" "message": "Loading"
},
"upgrade": {
"message": "Upgrade"
},
"upgradeOrganization": {
"message": "Upgrade Organization"
},
"upgradeOrganizationDesc": {
"message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features."
} }
} }