diff --git a/src/app/pipes/i18n.pipe.ts b/src/app/pipes/i18n.pipe.ts
index 0e0c3a1a1f9..2a6cef7cc0e 100644
--- a/src/app/pipes/i18n.pipe.ts
+++ b/src/app/pipes/i18n.pipe.ts
@@ -3,7 +3,7 @@ import {
PipeTransform,
} from '@angular/core';
-import { I18nService } from '../../services/i18n.service';
+import { I18nService } from 'jslib/abstractions/i18n.service';
@Pipe({
name: 'i18n',
diff --git a/src/app/services/services.module.ts b/src/app/services/services.module.ts
index c53e47938cc..02312c4476b 100644
--- a/src/app/services/services.module.ts
+++ b/src/app/services/services.module.ts
@@ -41,6 +41,7 @@ import {
CryptoService as CryptoServiceAbstraction,
EnvironmentService as EnvironmentServiceAbstraction,
FolderService as FolderServiceAbstraction,
+ I18nService as I18nServiceAbstraction,
LockService as LockServiceAbstraction,
MessagingService as MessagingServiceAbstraction,
PasswordGenerationService as PasswordGenerationServiceAbstraction,
@@ -58,7 +59,7 @@ webFrame.registerURLSchemeAsPrivileged('file');
const i18nService = new I18nService(window.navigator.language, './locales');
const utilsService = new UtilsService();
-const platformUtilsService = new DesktopPlatformUtilsService();
+const platformUtilsService = new DesktopPlatformUtilsService(i18nService);
const messagingService = new DesktopMessagingService();
const storageService: StorageServiceAbstraction = new DesktopStorageService();
const secureStorageService: StorageServiceAbstraction = new DesktopSecureStorageService();
@@ -109,11 +110,14 @@ function initFactory(i18n: I18nService): Function {
{ provide: EnvironmentServiceAbstraction, useValue: environmentService },
{ provide: TotpServiceAbstraction, useValue: totpService },
{ provide: TokenServiceAbstraction, useValue: tokenService },
- { provide: I18nService, useValue: i18nService },
+ { provide: I18nServiceAbstraction, useValue: i18nService },
+ { provide: UtilsServiceAbstraction, useValue: utilsService },
+ { provide: CryptoServiceAbstraction, useValue: cryptoService },
+ { provide: PlatformUtilsServiceAbstraction, useValue: platformUtilsService },
{
provide: APP_INITIALIZER,
useFactory: initFactory,
- deps: [I18nService],
+ deps: [I18nServiceAbstraction],
multi: true,
},
],
diff --git a/src/app/vault/view.component.html b/src/app/vault/view.component.html
index 4cd532b8565..fc85682f86b 100644
--- a/src/app/vault/view.component.html
+++ b/src/app/vault/view.component.html
@@ -9,6 +9,7 @@
{{'name' | i18n}}
{{cipher.name}}
+
@@ -51,7 +55,8 @@
@@ -69,6 +74,89 @@
{{totpCodeFormatted}}
+
+
+
+ {{'cardholderName' | i18n}}
+ {{cipher.card.cardholderName}}
+
+
+
+
{{'number' | i18n}}
+ {{cipher.card.number}}
+
+
+ {{'brand' | i18n}}
+ {{cipher.card.brand}}
+
+
+ {{'expiration' | i18n}}
+ {{cipher.card.expiration}}
+
+
+
+
{{'securityCode' | i18n}}
+ {{cipher.card.code}}
+
+
+
+
+
+ {{'identityName' | i18n}}
+ {{cipher.identity.fullName}}
+
+
+ {{'username' | i18n}}
+ {{cipher.identity.username}}
+
+
+ {{'company' | i18n}}
+ {{cipher.identity.company}}
+
+
+ {{'ssn' | i18n}}
+ {{cipher.identity.ssn}}
+
+
+ {{'passportNumber' | i18n}}
+ {{cipher.identity.passportNumber}}
+
+
+ {{'licenseNumber' | i18n}}
+ {{cipher.identity.licenseNumber}}
+
+
+ {{'email' | i18n}}
+ {{cipher.identity.email}}
+
+
+ {{'phone' | i18n}}
+ {{cipher.identity.phone}}
+
+
+
{{'address' | i18n}}
+
{{cipher.identity.address1}}
+
{{cipher.identity.address2}}
+
{{cipher.identity.address3}}
+
+ {{cipher.identity.city || '-'}},
+ {{cipher.identity.state || '-'}},
+ {{cipher.identity.postalCode || '-'}}
+
+
{{cipher.identity.country}}
+
+
@@ -84,7 +172,31 @@
{{'customFields' | i18n}}
- todo
+
+
+
{{field.name}}
+
+ {{field.value || ' '}}
+
+
+ {{field.value}}
+ {{field.maskedValue}}
+
+
+
+
+
+
@@ -92,7 +204,13 @@
{{'attachments' | i18n}}
diff --git a/src/app/vault/view.component.ts b/src/app/vault/view.component.ts
index e2b0d722c35..3a681b2ef3d 100644
--- a/src/app/vault/view.component.ts
+++ b/src/app/vault/view.component.ts
@@ -10,12 +10,19 @@ import {
} from '@angular/core';
import { CipherType } from 'jslib/enums/cipherType';
+import { FieldType } from 'jslib/enums/fieldType';
import { CipherService } from 'jslib/abstractions/cipher.service';
+import { CryptoService } from 'jslib/abstractions/crypto.service';
+import { I18nService } from 'jslib/abstractions/i18n.service';
+import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { TokenService } from 'jslib/abstractions/token.service';
import { TotpService } from 'jslib/abstractions/totp.service';
+import { UtilsService } from 'jslib/abstractions/utils.service';
+import { AttachmentView } from 'jslib/models/view/attachmentView';
import { CipherView } from 'jslib/models/view/cipherView';
+import { FieldView } from 'jslib/models/view/fieldView';
@Component({
selector: 'app-vault-view',
@@ -32,11 +39,14 @@ export class ViewComponent implements OnChanges, OnDestroy {
totpDash: number;
totpSec: number;
totpLow: boolean;
+ fieldType = FieldType;
private totpInterval: NodeJS.Timer;
constructor(private cipherService: CipherService, private totpService: TotpService,
- private tokenService: TokenService) {
+ private tokenService: TokenService, private utilsService: UtilsService,
+ private cryptoService: CryptoService, private platformUtilsService: PlatformUtilsService,
+ private i18nService: I18nService) {
}
async ngOnChanges() {
@@ -70,12 +80,57 @@ export class ViewComponent implements OnChanges, OnDestroy {
this.showPassword = !this.showPassword;
}
+ toggleFieldValue(field: FieldView) {
+ const f = (field as any);
+ f.showValue = !f.showValue;
+ }
+
launch() {
- // TODO
+ if (this.cipher.login.uri == null || this.cipher.login.uri.indexOf('://') === -1) {
+ return;
+ }
+
+ this.platformUtilsService.launchUri(this.cipher.login.uri);
}
copy(value: string) {
- // TODO
+ if (value == null) {
+ return;
+ }
+
+ this.utilsService.copyToClipboard(value, window.document);
+ }
+
+ async downloadAttachment(attachment: AttachmentView) {
+ const a = (attachment as any);
+ if (a.downloading) {
+ return;
+ }
+
+ if (!this.cipher.organizationId && !this.isPremium) {
+ this.platformUtilsService.alertError(this.i18nService.t('premiumRequired'),
+ this.i18nService.t('premiumRequiredDesc'));
+ return;
+ }
+
+ a.downloading = true;
+ const response = await fetch(new Request(attachment.url, { cache: 'no-cache' }));
+ if (response.status !== 200) {
+ this.platformUtilsService.alertError(null, this.i18nService.t('errorOccurred'));
+ a.downloading = false;
+ return;
+ }
+
+ try {
+ const buf = await response.arrayBuffer();
+ const key = await this.cryptoService.getOrgKey(this.cipher.organizationId);
+ const decBuf = await this.cryptoService.decryptFromBytes(buf, key);
+ this.platformUtilsService.saveFile(window, decBuf, null, attachment.fileName);
+ } catch (e) {
+ this.platformUtilsService.alertError(null, this.i18nService.t('errorOccurred'));
+ }
+
+ a.downloading = false;
}
private cleanUp() {
diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json
index 3a5ebe3b007..620e47640e5 100644
--- a/src/locales/en/messages.json
+++ b/src/locales/en/messages.json
@@ -91,5 +91,56 @@
},
"toggleVisibility": {
"message": "Toggle Visibility"
+ },
+ "cardholderName": {
+ "message": "Cardholder Name"
+ },
+ "number": {
+ "message": "Number"
+ },
+ "brand": {
+ "message": "Brand"
+ },
+ "expiration": {
+ "message": "Expiration"
+ },
+ "securityCode": {
+ "message": "Security Code"
+ },
+ "identityName": {
+ "message": "identityName"
+ },
+ "company": {
+ "message": "Company"
+ },
+ "ssn": {
+ "message": "Social Security Number"
+ },
+ "passportNumber": {
+ "message": "Passport Number"
+ },
+ "licenseNumber": {
+ "message": "License Number"
+ },
+ "email": {
+ "message": "Email"
+ },
+ "phone": {
+ "message": "Phone"
+ },
+ "address": {
+ "message": "Address"
+ },
+ "premiumRequired": {
+ "message": "Premium Required"
+ },
+ "premiumRequiredDesc": {
+ "message": "A premium membership is required to use this feature."
+ },
+ "errorOccurred": {
+ "message": "An error has occurred."
+ },
+ "error": {
+ "message": "Error"
}
}
diff --git a/src/main.ts b/src/main.ts
index b530aa71bd8..23226659dcf 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,4 +1,4 @@
-import { app, BrowserWindow, ipcMain, screen } from 'electron';
+import { app, BrowserWindow, dialog, ipcMain, screen } from 'electron';
import * as path from 'path';
import * as url from 'url';
/*
@@ -26,6 +26,10 @@ ipcMain.on('keytar', async (event: any, message: any) => {
});
*/
+ipcMain.on('showError', async (event: any, message: any) => {
+ dialog.showErrorBox(message.title, message.message);
+});
+
import { I18nService } from './services/i18n.service';
const i18nService = new I18nService('en', './locales/');
i18nService.init().then(() => { });
diff --git a/src/scss/styles.scss b/src/scss/styles.scss
index 485eb5488e2..2b93eab7b47 100644
--- a/src/scss/styles.scss
+++ b/src/scss/styles.scss
@@ -589,6 +589,9 @@ a {
padding: 10px 15px;
position: relative;
z-index: 1;
+ display: block;
+ color: $text-color;
+ overflow-wrap: break-word;
&:before {
content: "";
@@ -626,6 +629,12 @@ a {
overflow-x: auto;
}
+ .no-wrap {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
.row-label {
font-size: $font-size-small;
color: $text-muted;
@@ -666,6 +675,23 @@ a {
}
}
}
+
+ .right-icon {
+ float: right;
+ margin-top: 4px;
+ color: $list-icon-color;
+ }
+
+ .row-sub-label {
+ float: right;
+ display: block;
+ margin-right: 15px;
+ color: $gray-light;
+ }
+
+ small.row-sub-label {
+ margin-top: 2px;
+ }
}
}
diff --git a/src/services/desktopPlatformUtils.service.ts b/src/services/desktopPlatformUtils.service.ts
index 6894743405d..112fcddab74 100644
--- a/src/services/desktopPlatformUtils.service.ts
+++ b/src/services/desktopPlatformUtils.service.ts
@@ -1,6 +1,9 @@
+import { ipcRenderer, shell } from 'electron';
+
import { DeviceType } from 'jslib/enums';
-import { PlatformUtilsService } from 'jslib/abstractions';
+import { I18nService } from 'jslib/abstractions/i18n.service';
+import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
const AnalyticsIds = {
[DeviceType.Windows]: 'UA-81915606-17',
@@ -12,6 +15,9 @@ export class DesktopPlatformUtilsService implements PlatformUtilsService {
private deviceCache: DeviceType = null;
private analyticsIdCache: string = null;
+ constructor(private i18nService: I18nService) {
+ }
+
getDevice(): DeviceType {
if (!this.deviceCache) {
switch (process.platform) {
@@ -93,4 +99,25 @@ export class DesktopPlatformUtilsService implements PlatformUtilsService {
isViewOpen(): boolean {
return true;
}
+
+ launchUri(uri: string): void {
+ shell.openExternal(uri);
+ }
+
+ saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void {
+ const blob = new Blob([blobData], blobOptions);
+ const a = win.document.createElement('a');
+ a.href = win.URL.createObjectURL(blob);
+ a.download = fileName;
+ window.document.body.appendChild(a);
+ a.click();
+ window.document.body.removeChild(a);
+ }
+
+ alertError(title: string, message: string): void {
+ ipcRenderer.send('showError', {
+ title: title || this.i18nService.t('error'),
+ message: message,
+ });
+ }
}
diff --git a/src/services/i18n.service.ts b/src/services/i18n.service.ts
index 621abad8b79..97fcd2521d7 100644
--- a/src/services/i18n.service.ts
+++ b/src/services/i18n.service.ts
@@ -1,11 +1,13 @@
import * as path from 'path';
+import { I18nService as I18nServiceAbstraction } from 'jslib/abstractions/i18n.service';
+
// First locale is the default (English)
const SupportedLocales = [
'en', 'es',
];
-export class I18nService {
+export class I18nService implements I18nServiceAbstraction {
defaultMessages: any = {};
localeMessages: any = {};
language: string;