From 3e408c4ea784ee123583428a60491c6e2b165c6b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 2 Feb 2018 12:01:55 -0500 Subject: [PATCH] i18n placeholder support. 2fa options selection. --- .../two-factor-options.component.html | 28 +++++++ .../accounts/two-factor-options.component.ts | 70 ++++++++++++++++++ src/app/accounts/two-factor.component.html | 14 +++- src/app/accounts/two-factor.component.ts | 39 ++++++++-- src/app/app.module.ts | 4 +- src/app/pipes/i18n.pipe.ts | 7 +- src/images/two-factor/0.png | Bin 0 -> 6054 bytes src/images/two-factor/1.png | Bin 0 -> 2675 bytes src/images/two-factor/2.png | Bin 0 -> 1211 bytes src/images/two-factor/3.png | Bin 0 -> 1624 bytes src/images/two-factor/4.png | Bin 0 -> 4810 bytes src/locales/en/messages.json | 29 +++++++- src/scss/box.scss | 17 ++++- src/scss/misc.scss | 14 +++- src/scss/pages.scss | 9 +++ src/services/i18n.service.ts | 49 ++++++++++-- webpack.renderer.js | 4 +- 17 files changed, 254 insertions(+), 30 deletions(-) create mode 100644 src/app/accounts/two-factor-options.component.html create mode 100644 src/app/accounts/two-factor-options.component.ts create mode 100644 src/images/two-factor/0.png create mode 100644 src/images/two-factor/1.png create mode 100644 src/images/two-factor/2.png create mode 100644 src/images/two-factor/3.png create mode 100644 src/images/two-factor/4.png diff --git a/src/app/accounts/two-factor-options.component.html b/src/app/accounts/two-factor-options.component.html new file mode 100644 index 0000000000..740365d1ea --- /dev/null +++ b/src/app/accounts/two-factor-options.component.html @@ -0,0 +1,28 @@ + diff --git a/src/app/accounts/two-factor-options.component.ts b/src/app/accounts/two-factor-options.component.ts new file mode 100644 index 0000000000..acdb4a10d3 --- /dev/null +++ b/src/app/accounts/two-factor-options.component.ts @@ -0,0 +1,70 @@ +import * as template from './two-factor-options.component.html'; + +import { + Component, + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; + +import { Router } from '@angular/router'; + +import { Angulartics2 } from 'angulartics2'; +import { ToasterService } from 'angular2-toaster'; + +import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType'; + +import { AuthService } from 'jslib/abstractions/auth.service'; +import { I18nService } from 'jslib/abstractions/i18n.service'; +import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; + +import { TwoFactorProviders } from 'jslib/services/auth.service'; + +@Component({ + selector: 'app-two-factor-options', + template: template, +}) +export class TwoFactorOptionsComponent implements OnInit { + @Output() onProviderSelected = new EventEmitter(); + @Output() onRecoverSelected = new EventEmitter(); + + providers: any[] = []; + + constructor(private authService: AuthService, private router: Router, private analytics: Angulartics2, + private toasterService: ToasterService, private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService) { } + + ngOnInit() { + if (this.authService.twoFactorProviders.has(TwoFactorProviderType.Authenticator)) { + this.providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]); + } + + if (this.authService.twoFactorProviders.has(TwoFactorProviderType.Yubikey)) { + this.providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]); + } + + if (this.authService.twoFactorProviders.has(TwoFactorProviderType.Duo)) { + this.providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]); + } + + if (this.authService.twoFactorProviders.has(TwoFactorProviderType.U2f) && + this.platformUtilsService.supportsU2f(window)) { + this.providers.push(TwoFactorProviders[TwoFactorProviderType.U2f]); + } + + if (this.authService.twoFactorProviders.has(TwoFactorProviderType.Email)) { + this.providers.push(TwoFactorProviders[TwoFactorProviderType.Email]); + } + } + + choose(p: any) { + this.onProviderSelected.emit(p.type); + } + + recover() { + this.analytics.eventTrack.next({ action: 'Selected Recover' }); + this.platformUtilsService.launchUri('https://help.bitwarden.com/article/lost-two-step-device/'); + this.onRecoverSelected.emit(); + } +} diff --git a/src/app/accounts/two-factor.component.html b/src/app/accounts/two-factor.component.html index b70909afc8..6adfd9a471 100644 --- a/src/app/accounts/two-factor.component.html +++ b/src/app/accounts/two-factor.component.html @@ -2,7 +2,9 @@

{{title}}

{{'enterVerificationCodeApp' | i18n}}

-

{{'enterVerificationCodeEmail' | i18n}}

+

+ {{'enterVerificationCodeEmail' | i18n : twoFactorEmail}} +

@@ -43,7 +45,7 @@
-
+

{{'noTwoStepProviders' | i18n}}

@@ -52,7 +54,8 @@
- @@ -60,6 +63,11 @@
+ diff --git a/src/app/accounts/two-factor.component.ts b/src/app/accounts/two-factor.component.ts index 4782cdce54..e595c5de17 100644 --- a/src/app/accounts/two-factor.component.ts +++ b/src/app/accounts/two-factor.component.ts @@ -2,7 +2,10 @@ import * as template from './two-factor.component.html'; import { Component, + ComponentFactoryResolver, OnInit, + ViewChild, + ViewContainerRef, } from '@angular/core'; import { Router } from '@angular/router'; @@ -10,6 +13,9 @@ import { Router } from '@angular/router'; import { Angulartics2 } from 'angulartics2'; import { ToasterService } from 'angular2-toaster'; +import { TwoFactorOptionsComponent } from './two-factor-options.component'; +import { ModalComponent } from '../modal.component'; + import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType'; import { TwoFactorEmailRequest } from 'jslib/models/request/twoFactorEmailRequest'; @@ -26,6 +32,8 @@ import { TwoFactorProviders } from 'jslib/services/auth.service'; template: template, }) export class TwoFactorComponent implements OnInit { + @ViewChild('twoFactorOptions', { read: ViewContainerRef }) twoFactorOptionsModal: ViewContainerRef; + token: string = ''; remember: boolean = false; u2fReady: boolean = false; @@ -35,14 +43,14 @@ export class TwoFactorComponent implements OnInit { u2fSupported: boolean = false; u2f: any = null; title: string = ''; - useVerificationCode: boolean = false; twoFactorEmail: string = null; formPromise: Promise; emailPromise: Promise; constructor(private authService: AuthService, private router: Router, private analytics: Angulartics2, private toasterService: ToasterService, private i18nService: I18nService, private apiService: ApiService, - private platformUtilsService: PlatformUtilsService) { + private platformUtilsService: PlatformUtilsService, + private componentFactoryResolver: ComponentFactoryResolver) { this.u2fSupported = this.platformUtilsService.supportsU2f(window); } @@ -58,10 +66,6 @@ export class TwoFactorComponent implements OnInit { } async init() { - this.useVerificationCode = this.selectedProviderType === TwoFactorProviderType.Email || - this.selectedProviderType === TwoFactorProviderType.Authenticator || - this.selectedProviderType === TwoFactorProviderType.Yubikey; - if (this.selectedProviderType == null) { this.title = this.i18nService.t('loginUnavailable'); return; @@ -135,17 +139,36 @@ export class TwoFactorComponent implements OnInit { return; } + if (this.emailPromise != null) { + return; + } + try { const request = new TwoFactorEmailRequest(this.authService.email, this.authService.masterPasswordHash); this.emailPromise = this.apiService.postTwoFactorEmail(request); await this.emailPromise; if (doToast) { - // TODO toast + this.toasterService.popAsync('success', null, + this.i18nService.t('verificationCodeEmailSent', this.twoFactorEmail)); } } catch { } + + this.emailPromise = null; } anotherMethod() { - // TODO + const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + const modal = this.twoFactorOptionsModal.createComponent(factory).instance; + const childComponent = modal.show(TwoFactorOptionsComponent, + this.twoFactorOptionsModal); + + childComponent.onProviderSelected.subscribe(async (provider: TwoFactorProviderType) => { + modal.close(); + this.selectedProviderType = provider; + await this.init(); + }); + childComponent.onRecoverSelected.subscribe(() => { + modal.close(); + }); } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index bec35815a2..ebc3e39828 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -32,6 +32,7 @@ import { SearchCiphersPipe } from './pipes/search-ciphers.pipe'; import { StopClickDirective } from './directives/stop-click.directive'; import { StopPropDirective } from './directives/stop-prop.directive'; import { TwoFactorComponent } from './accounts/two-factor.component'; +import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component'; import { VaultComponent } from './vault/vault.component'; import { ViewComponent } from './vault/view.component'; @@ -71,15 +72,16 @@ import { ViewComponent } from './vault/view.component'; StopClickDirective, StopPropDirective, TwoFactorComponent, + TwoFactorOptionsComponent, VaultComponent, ViewComponent, - WebviewDirective, ], entryComponents: [ AttachmentsComponent, FolderAddEditComponent, ModalComponent, PasswordGeneratorComponent, + TwoFactorOptionsComponent, ], providers: [], bootstrap: [AppComponent], diff --git a/src/app/pipes/i18n.pipe.ts b/src/app/pipes/i18n.pipe.ts index 2a6cef7cc0..d542af9347 100644 --- a/src/app/pipes/i18n.pipe.ts +++ b/src/app/pipes/i18n.pipe.ts @@ -9,10 +9,9 @@ import { I18nService } from 'jslib/abstractions/i18n.service'; name: 'i18n', }) export class I18nPipe implements PipeTransform { - constructor(private i18nService: I18nService) { - } + constructor(private i18nService: I18nService) { } - transform(id: string): string { - return this.i18nService.t(id); + transform(id: string, p1?: string, p2?: string, p3?: string): string { + return this.i18nService.t(id, p1, p2, p3); } } diff --git a/src/images/two-factor/0.png b/src/images/two-factor/0.png new file mode 100644 index 0000000000000000000000000000000000000000..f37e3f17b4dfbb57eff732342ce64c4625bf4ffe GIT binary patch literal 6054 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 literal 0 HcmV?d00001 diff --git a/src/images/two-factor/2.png b/src/images/two-factor/2.png new file mode 100644 index 0000000000000000000000000000000000000000..ab2e43403614fdae987a9f34b5c7c80f7a0599cd GIT binary patch literal 1211 zcmV;s1VsCZP)%tn11b)cMr&h>9Z! zqKKearD~K`BZ~9TI#Wu;p;hXDB*uIFf9`+q5R%;C+O!F7NG5NB{Cmb2|{b3iEUsr5Tm;$;&uhR z2fc2G2CIo6P80k_e=OA>3pwGwef0zL1Gpuq6#TzyWbho?>mx__+N( z+VEYxp&$@NXd@pcF;v_=8 zKa-nV#d;|v2BXw_XnM&1;JHhkxLJD0E8oirdtT$Ver>VbGhr|y_yTy;`LDdA+J&=s z)R^%g=_&Xde5~*lcEq@08M`?OZ^$!fCyhf^k-pw^Hd{Ir~yZyf3{RxgoK#*hd+?FBUvs44}p_L$Da;4jAH6=O+nm{UqwO>AjDnK6?Ue_p+&`vL5c;ymrC<9f6;HrR?aO zMs~hKO||F>#(_~T^=|;3mASpu{c{zhHpA;rie3_h^p*#B=Snpil-CuO&bl;J@1DIE ZU;w~$PbXFRCwC# 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 literal 0 HcmV?d00001 diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 738f4cc9e1..bdc2b77b32 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -459,7 +459,22 @@ "message": "Enter the 6 digit verification code from your authenticator app." }, "enterVerificationCodeEmail": { - "message": "Enter the 6 digit verification code that was emailed to" + "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", + "placeholders": { + "email": { + "content": "$1", + "example": "example@gmail.com" + } + } + }, + "verificationCodeEmailSent": { + "message": "Verification email sent to $EMAIL$.", + "placeholders": { + "email": { + "content": "$1", + "example": "example@gmail.com" + } + } }, "rememberMe": { "message": "Remember me" @@ -510,5 +525,17 @@ }, "emailDesc": { "message": "Verification codes will be emailed to you." + }, + "loginUnavailable": { + "message": "Login Unavailable" + }, + "noTwoStepProviders": { + "message": "This account has two-step login enabled, however, none of the configured two-step providers are supported by this device." + }, + "noTwoStepProviders2": { + "message": "Please add additional providers that are better supported across devices (such as an authenticator app)." + }, + "twoStepOptions": { + "message": "Two-step Login Options" } } diff --git a/src/scss/box.scss b/src/scss/box.scss index 94cf6a1f1d..6b33725a0b 100644 --- a/src/scss/box.scss +++ b/src/scss/box.scss @@ -26,8 +26,6 @@ padding: 10px 15px; position: relative; z-index: 1; - overflow-wrap: break-word; - word-break: break-all; &:before { content: ""; @@ -73,6 +71,21 @@ margin-bottom: 5px; } + .text, .detail { + display: block; + color: $text-color; + } + + .detail { + font-size: $font-size-small; + color: $text-muted; + } + + .img-right { + float: right; + margin-left: 10px; + } + .row-main { flex-grow: 1; } diff --git a/src/scss/misc.scss b/src/scss/misc.scss index 1d900075cd..754c045880 100644 --- a/src/scss/misc.scss +++ b/src/scss/misc.scss @@ -106,8 +106,8 @@ #duo-frame { background: url('../images/loading.svg') 0 0 no-repeat; - width: 100%; - height: 300px; + height: 330px; + margin: 0 -150px 15px -150px; iframe { width: 100%; @@ -115,3 +115,13 @@ border: none; } } + +@keyframes spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} diff --git a/src/scss/pages.scss b/src/scss/pages.scss index 40ad264317..b6abec1fb1 100644 --- a/src/scss/pages.scss +++ b/src/scss/pages.scss @@ -78,6 +78,15 @@ .sub-options { text-align: center; margin-bottom: 20px; + + a { + display: block; + margin-bottom: 10px; + + &:last-child { + margin-bottom: 0; + } + } } a.settings-icon { diff --git a/src/services/i18n.service.ts b/src/services/i18n.service.ts index 5b9d3dad71..fadc58faa5 100644 --- a/src/services/i18n.service.ts +++ b/src/services/i18n.service.ts @@ -41,17 +41,33 @@ export class I18nService implements I18nServiceAbstraction { } } - t(id: string): string { - return this.translate(id); + t(id: string, p1?: string, p2?: string, p3?: string): string { + return this.translate(id, p1, p2, p3); } - translate(id: string): string { + translate(id: string, p1?: string, p2?: string, p3?: string): string { + let result: string; if (this.localeMessages.hasOwnProperty(id) && this.localeMessages[id]) { - return this.localeMessages[id]; + result = this.localeMessages[id]; } else if (this.defaultMessages.hasOwnProperty(id) && this.defaultMessages[id]) { - return this.defaultMessages[id]; + result = this.defaultMessages[id]; + } else { + result = ''; } - return ''; + + if (result !== '') { + if (p1 != null) { + result = result.split('__$1__').join(p1); + } + if (p2 != null) { + result = result.split('__$2__').join(p2); + } + if (p3 != null) { + result = result.split('__$3__').join(p3); + } + } + + return result; } private loadMessages(locale: string, messagesObj: any): Promise { @@ -59,8 +75,25 @@ export class I18nService implements I18nServiceAbstraction { const filePath = path.join(__dirname, this.localesDirectory + '/' + formattedLocale + '/messages.json'); const locales = (window as any).require(filePath); for (const prop in locales) { - if (locales.hasOwnProperty(prop)) { - messagesObj[prop] = locales[prop].message; + if (!locales.hasOwnProperty(prop)) { + continue; + } + messagesObj[prop] = locales[prop].message; + + if (locales[prop].placeholders) { + for (const placeProp in locales[prop].placeholders) { + if (!locales[prop].placeholders.hasOwnProperty(placeProp) || + !locales[prop].placeholders[placeProp].content) { + continue; + } + + const replaceToken = '\\$' + placeProp.toUpperCase() + '\\$'; + let replaceContent = locales[prop].placeholders[placeProp].content; + if (replaceContent === '$1' || replaceContent === '$2' || replaceContent === '$3') { + replaceContent = '__' + replaceContent + '__'; + } + messagesObj[prop] = messagesObj[prop].replace(new RegExp(replaceToken, 'g'), replaceContent); + } } } diff --git a/webpack.renderer.js b/webpack.renderer.js index 66d2912d52..e9aa03df88 100644 --- a/webpack.renderer.js +++ b/webpack.renderer.js @@ -76,6 +76,7 @@ const renderer = { }, { test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, + exclude: /loading.svg/, use: [{ loader: 'file-loader', options: { @@ -95,7 +96,8 @@ const renderer = { { loader: 'sass-loader', } - ] + ], + publicPath: '../' }) }, ]