diff --git a/src/app/accounts/premium.component.html b/src/app/accounts/premium.component.html new file mode 100644 index 0000000000..a796cbe2e5 --- /dev/null +++ b/src/app/accounts/premium.component.html @@ -0,0 +1,62 @@ + diff --git a/src/app/accounts/premium.component.ts b/src/app/accounts/premium.component.ts new file mode 100644 index 0000000000..88ae8e46fe --- /dev/null +++ b/src/app/accounts/premium.component.ts @@ -0,0 +1,59 @@ +import * as template from './premium.component.html'; + +import { + Component, + OnInit, +} 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 { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; +import { TokenService } from 'jslib/abstractions/token.service'; + +@Component({ + selector: 'app-premium', + template: template, +}) +export class PremiumComponent implements OnInit { + isPremium: boolean = false; + price: string = '$10'; + refreshPromise: Promise; + + constructor(private analytics: Angulartics2, private toasterService: ToasterService, + private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, + private tokenService: TokenService, private apiService: ApiService) { } + + async ngOnInit() { + this.isPremium = !this.tokenService.getPremium(); + } + + async refresh() { + try { + this.refreshPromise = this.apiService.refreshIdentityToken(); + await this.refreshPromise; + this.toasterService.popAsync('success', null, this.i18nService.t('refreshComplete')); + this.isPremium = this.tokenService.getPremium(); + } catch { } + } + + async purchase() { + const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('premiumPurchaseAlert'), + this.i18nService.t('premiumPurchase'), this.i18nService.t('yes'), this.i18nService.t('cancel')); + if (confirmed) { + this.analytics.eventTrack.next({ action: 'Clicked Purchase Premium' }); + this.platformUtilsService.launchUri('https://vault.bitwarden.com/#/?premium=purchase'); + } + } + + async manage() { + const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('premiumManageAlert'), + this.i18nService.t('premiumManage'), this.i18nService.t('yes'), this.i18nService.t('cancel')); + if (confirmed) { + this.analytics.eventTrack.next({ action: 'Clicked Manage Membership' }); + this.platformUtilsService.launchUri('https://vault.bitwarden.com/#/?premium=manage'); + } + } +} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 27387325cd..5ccb1f646f 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -17,6 +17,7 @@ import { Router } from '@angular/router'; import { ModalComponent } from './modal.component'; +import { PremiumComponent } from './accounts/premium.component'; import { SettingsComponent } from './accounts/settings.component'; import { ToasterService } from 'angular2-toaster'; @@ -49,10 +50,12 @@ const BroadcasterSubscriptionId = 'AppComponent'; template: ` + `, }) export class AppComponent implements OnInit { @ViewChild('settings', { read: ViewContainerRef }) settingsRef: ViewContainerRef; + @ViewChild('premium', { read: ViewContainerRef }) premiumRef: ViewContainerRef; toasterConfig: ToasterConfig = new ToasterConfig({ showCloseButton: true, @@ -113,6 +116,9 @@ export class AppComponent implements OnInit { case 'openSettings': this.openSettings(); break; + case 'openPremium': + this.openPremium(); + break; default: } }); @@ -177,4 +183,18 @@ export class AppComponent implements OnInit { this.modal = null; }); } + + private openPremium() { + if (this.modal != null) { + this.modal.close(); + } + + const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + this.modal = this.premiumRef.createComponent(factory).instance; + const childComponent = this.modal.show(PremiumComponent, this.premiumRef); + + this.modal.onClosed.subscribe(() => { + this.modal = null; + }); + } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index bf93536076..e1bb7a4230 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -20,6 +20,7 @@ import { EnvironmentComponent } from './accounts/environment.component'; import { HintComponent } from './accounts/hint.component'; import { LockComponent } from './accounts/lock.component'; import { LoginComponent } from './accounts/login.component'; +import { PremiumComponent } from './accounts/premium.component'; import { RegisterComponent } from './accounts/register.component'; import { SettingsComponent } from './accounts/settings.component'; import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component'; @@ -80,6 +81,7 @@ import { ViewComponent } from './vault/view.component'; LoginComponent, ModalComponent, PasswordGeneratorComponent, + PremiumComponent, RegisterComponent, SearchCiphersPipe, SettingsComponent, @@ -96,6 +98,7 @@ import { ViewComponent } from './vault/view.component'; FolderAddEditComponent, ModalComponent, PasswordGeneratorComponent, + PremiumComponent, SettingsComponent, TwoFactorOptionsComponent, ], diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index fcfe9f554c..ee92177dfa 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -650,9 +650,6 @@ "syncVault": { "message": "Sync Vault" }, - "premiumMembership": { - "message": "Premium Membership" - }, "changeMasterPass": { "message": "Change Master Password" }, @@ -814,5 +811,62 @@ "copySecurityCode": { "message": "Copy Security Code", "description": "Copy credit card security code (CVV)" + }, + "premiumMembership": { + "message": "Premium Membership" + }, + "premiumManage": { + "message": "Manage Membership" + }, + "premiumManageAlert": { + "message": "You can manage your membership on the bitwarden.com web vault. Do you want to visit the website now?" + }, + "premiumRefresh": { + "message": "Refresh Membership" + }, + "premiumNotCurrentMember": { + "message": "You are not currently a premium member." + }, + "premiumSignUpAndGet": { + "message": "Sign up for a premium membership and get:" + }, + "premiumSignUpStorage": { + "message": "1 GB of encrypted file storage." + }, + "premiumSignUpTwoStep": { + "message": "Additional two-step login options such as YubiKey, FIDO U2F, and Duo." + }, + "premiumSignUpTotp": { + "message": "TOTP verification code (2FA) generator for logins in your vault." + }, + "premiumSignUpSupport": { + "message": "Priority customer support." + }, + "premiumSignUpFuture": { + "message": "All future premium features. More coming soon!" + }, + "premiumPurchase": { + "message": "Purchase Premium" + }, + "premiumPurchaseAlert": { + "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" + }, + "premiumCurrentMember": { + "message": "You are a premium member!" + }, + "premiumCurrentMemberThanks": { + "message": "Thank you for supporting bitwarden." + }, + "premiumPrice": { + "message": "All for just $PRICE$ /year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, + "refreshComplete": { + "message": "Refresh complete" } } diff --git a/src/main/menu.main.ts b/src/main/menu.main.ts index ca2702958b..9ab0a3385e 100644 --- a/src/main/menu.main.ts +++ b/src/main/menu.main.ts @@ -203,7 +203,7 @@ export class MenuMain { submenu: [ { label: this.main.i18nService.t('premiumMembership'), - click: () => this.main.messagingService.send('premiumMembership'), + click: () => this.main.messagingService.send('openPremium'), id: 'premiumMembership', }, { diff --git a/src/scss/base.scss b/src/scss/base.scss index e1ecfe4bff..c6512be2b4 100644 --- a/src/scss/base.scss +++ b/src/scss/base.scss @@ -24,6 +24,10 @@ p { margin-bottom: 10px; } +ul, ol { + margin-bottom: 10px; +} + img { border: none; } diff --git a/src/scss/misc.scss b/src/scss/misc.scss index 28d07a0f11..2c34b39ab3 100644 --- a/src/scss/misc.scss +++ b/src/scss/misc.scss @@ -8,6 +8,10 @@ small { color: $brand-primary !important; } +.text-success { + color: $brand-success !important; +} + .text-muted { color: $text-muted !important; } @@ -20,6 +24,16 @@ small { text-align: center; } +.no-margin { + margin: 0 !important; +} + +p.lead { + font-size: $font-size-large; + margin-bottom: 20px; + font-weight: normal; +} + [hidden] { display: none !important; }